mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-07-17 01:37:23 +02:00
feat: Mark as seen on typing
feat: Save to RyukGram album feat: Hide trailing action buttons on preserved messages feat: Confirmation when pulling to refresh the DMs tab if preserved messages would be cleared
This commit is contained in:
+20
-20
@@ -1,4 +1,5 @@
|
||||
#import "Download.h"
|
||||
#import "../PhotoAlbum.h"
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
#pragma mark - SCIDownloadPillView
|
||||
@@ -217,30 +218,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||
NSString *ext = [[fileURL pathExtension] lowercaseString];
|
||||
BOOL isVideo = [@[@"mp4", @"mov", @"m4v"] containsObject:ext];
|
||||
|
||||
if (isVideo) {
|
||||
PHAssetCreationRequest *req = [PHAssetCreationRequest creationRequestForAsset];
|
||||
PHAssetResourceCreationOptions *opts = [[PHAssetResourceCreationOptions alloc] init];
|
||||
opts.shouldMoveFile = YES;
|
||||
[req addResourceWithType:PHAssetResourceTypeVideo fileURL:fileURL options:opts];
|
||||
req.creationDate = [NSDate date];
|
||||
} else {
|
||||
PHAssetCreationRequest *req = [PHAssetCreationRequest creationRequestForAsset];
|
||||
PHAssetResourceCreationOptions *opts = [[PHAssetResourceCreationOptions alloc] init];
|
||||
opts.shouldMoveFile = YES;
|
||||
[req addResourceWithType:PHAssetResourceTypePhoto fileURL:fileURL options:opts];
|
||||
req.creationDate = [NSDate date];
|
||||
}
|
||||
} completionHandler:^(BOOL success, NSError *error) {
|
||||
BOOL useAlbum = [SCIUtils getBoolPref:@"save_to_ryukgram_album"];
|
||||
void (^onDone)(BOOL, NSError *) = ^(BOOL success, NSError *error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (success) {
|
||||
SCIDownloadPillView *donePill = [[SCIDownloadPillView alloc] init];
|
||||
donePill.progressRing.hidden = YES;
|
||||
donePill.subtitleLabel.text = nil;
|
||||
[donePill setText:@"Saved to Photos"];
|
||||
[donePill setText:useAlbum ? @"Saved to RyukGram" : @"Saved to Photos"];
|
||||
UIView *hostView = topMostController().view;
|
||||
if (hostView) {
|
||||
[donePill showInView:hostView];
|
||||
@@ -250,7 +235,22 @@
|
||||
[SCIUtils showErrorHUDWithDescription:@"Failed to save to Photos"];
|
||||
}
|
||||
});
|
||||
}];
|
||||
};
|
||||
|
||||
if (useAlbum) {
|
||||
[SCIPhotoAlbum saveFileToAlbum:fileURL completion:onDone];
|
||||
} else {
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||
NSString *ext = [[fileURL pathExtension] lowercaseString];
|
||||
BOOL isVideo = [@[@"mp4", @"mov", @"m4v"] containsObject:ext];
|
||||
PHAssetCreationRequest *req = [PHAssetCreationRequest creationRequestForAsset];
|
||||
PHAssetResourceCreationOptions *opts = [[PHAssetResourceCreationOptions alloc] init];
|
||||
opts.shouldMoveFile = YES;
|
||||
[req addResourceWithType:(isVideo ? PHAssetResourceTypeVideo : PHAssetResourceTypePhoto)
|
||||
fileURL:fileURL options:opts];
|
||||
req.creationDate = [NSDate date];
|
||||
} completionHandler:onDone];
|
||||
}
|
||||
}];
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
// Defined in SeenButtons.x
|
||||
extern __weak IGDirectThreadViewController *sciActiveThreadVC;
|
||||
extern BOOL sciAutoTypingEnabled(void);
|
||||
extern void sciDoAutoSeen(IGDirectThreadViewController *threadVC);
|
||||
|
||||
%hook IGDirectTypingStatusService
|
||||
- (void)updateOutgoingStatusIsActive:(_Bool)active threadKey:(id)key threadMetadata:(id)metadata typingStatusType:(long long)type {
|
||||
// Mark the visible thread as seen on the first typing event — runs even
|
||||
// when typing-status broadcasting is blocked below.
|
||||
if (active && sciAutoTypingEnabled()) {
|
||||
IGDirectThreadViewController *vc = sciActiveThreadVC;
|
||||
if (vc) sciDoAutoSeen(vc);
|
||||
}
|
||||
|
||||
if ([SCIUtils getBoolPref:@"disable_typing_status"]) return;
|
||||
|
||||
return %orig(active, key, metadata, type);
|
||||
|
||||
@@ -140,13 +140,7 @@ static id new_prismMenuView_init3(id self, SEL _cmd, NSArray *elements, id heade
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[pill setText:@"Done!"];
|
||||
[pill dismissAfterDelay:0.5];
|
||||
|
||||
UIActivityViewController *shareVC = [[UIActivityViewController alloc]
|
||||
initWithActivityItems:@[finalURL]
|
||||
applicationActivities:nil];
|
||||
UIViewController *top = [UIApplication sharedApplication].keyWindow.rootViewController;
|
||||
while (top.presentedViewController) top = top.presentedViewController;
|
||||
[top presentViewController:shareVC animated:YES completion:nil];
|
||||
[SCIUtils showShareVC:finalURL];
|
||||
});
|
||||
}];
|
||||
}];
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Pull-to-refresh in the DMs tab silently clears preserved (locally retained)
|
||||
// unsent messages. This hook intercepts _pullToRefreshIfPossible to show a
|
||||
// confirmation dialog when both keep_deleted_message and
|
||||
// warn_refresh_clears_preserved are on.
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import <objc/runtime.h>
|
||||
#import <objc/message.h>
|
||||
#import <substrate.h>
|
||||
|
||||
extern NSMutableSet *sciGetPreservedIds(void);
|
||||
extern void sciClearPreservedIds(void);
|
||||
|
||||
static BOOL sciRefreshConfirmInFlight = NO;
|
||||
static BOOL sciRefreshAlertVisible = NO;
|
||||
|
||||
static UIRefreshControl *sciFindRefreshControl(UIViewController *vc) {
|
||||
Class igRC = NSClassFromString(@"IGRefreshControl");
|
||||
NSMutableArray *stack = [NSMutableArray arrayWithObject:vc.view];
|
||||
while (stack.count > 0) {
|
||||
UIView *v = stack.lastObject;
|
||||
[stack removeLastObject];
|
||||
if ((igRC && [v isKindOfClass:igRC]) || [v isKindOfClass:[UIRefreshControl class]]) {
|
||||
return (UIRefreshControl *)v;
|
||||
}
|
||||
for (UIView *sub in v.subviews) [stack addObject:sub];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// On cancel, the IGRefreshControl's state machine is already idle by the time
|
||||
// our handler runs — but the scroll view's contentInset stays expanded, leaving
|
||||
// the spinner area visually exposed. We grab the idle inset via the inbox VC's
|
||||
// idleTopContentInsetForRefreshControl: helper and animate the inset back.
|
||||
static void sciCancelRefresh(UIViewController *vc) {
|
||||
UIRefreshControl *rc = sciFindRefreshControl(vc);
|
||||
if (!rc) return;
|
||||
|
||||
Ivar stateIvar = class_getInstanceVariable([rc class], "_refreshState");
|
||||
if (stateIvar) {
|
||||
ptrdiff_t off = ivar_getOffset(stateIvar);
|
||||
*(NSInteger *)((char *)(__bridge void *)rc + off) = 0;
|
||||
}
|
||||
Ivar animIvar = class_getInstanceVariable([rc class], "_swiftAnimationInfo");
|
||||
if (animIvar) object_setIvar(rc, animIvar, nil);
|
||||
if ([rc respondsToSelector:@selector(endRefreshing)]) [rc endRefreshing];
|
||||
|
||||
SEL didEnd = NSSelectorFromString(@"refreshControlDidEndFinishLoadingAnimation:");
|
||||
if ([vc respondsToSelector:didEnd]) {
|
||||
((void(*)(id, SEL, id))objc_msgSend)(vc, didEnd, rc);
|
||||
}
|
||||
|
||||
UIScrollView *scroll = nil;
|
||||
UIView *cur = rc.superview;
|
||||
while (cur) {
|
||||
if ([cur isKindOfClass:[UIScrollView class]]) { scroll = (UIScrollView *)cur; break; }
|
||||
cur = cur.superview;
|
||||
}
|
||||
if (scroll) {
|
||||
SEL idleSel = NSSelectorFromString(@"idleTopContentInsetForRefreshControl:");
|
||||
CGFloat idleInset = scroll.contentInset.top;
|
||||
if ([vc respondsToSelector:idleSel]) {
|
||||
idleInset = ((CGFloat(*)(id, SEL, id))objc_msgSend)(vc, idleSel, rc);
|
||||
}
|
||||
UIEdgeInsets insets = scroll.contentInset;
|
||||
insets.top = idleInset;
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
scroll.contentInset = insets;
|
||||
CGPoint o = scroll.contentOffset;
|
||||
if (o.y < -idleInset) o.y = -idleInset;
|
||||
scroll.contentOffset = o;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
static void (*orig_pullToRefresh)(id self, SEL _cmd);
|
||||
static void new_pullToRefresh(id self, SEL _cmd) {
|
||||
if (sciRefreshConfirmInFlight ||
|
||||
![SCIUtils getBoolPref:@"keep_deleted_message"] ||
|
||||
![SCIUtils getBoolPref:@"warn_refresh_clears_preserved"]) {
|
||||
orig_pullToRefresh(self, _cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
// IG fires _pullToRefreshIfPossible repeatedly while the user holds the
|
||||
// pull gesture — drop re-entrant calls until the alert is dismissed.
|
||||
if (sciRefreshAlertVisible) return;
|
||||
|
||||
NSUInteger count = sciGetPreservedIds().count;
|
||||
if (count == 0) {
|
||||
orig_pullToRefresh(self, _cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
UIViewController *vc = (UIViewController *)self;
|
||||
NSString *msg = [NSString stringWithFormat:
|
||||
@"Refreshing the DMs tab will clear %lu preserved unsent message%@. This cannot be undone.",
|
||||
(unsigned long)count, count == 1 ? @"" : @"s"];
|
||||
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Clear preserved messages?"
|
||||
message:msg
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
__weak UIViewController *weakSelf = vc;
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *a) {
|
||||
sciCancelRefresh(weakSelf);
|
||||
sciRefreshAlertVisible = NO;
|
||||
}]];
|
||||
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Refresh" style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction *a) {
|
||||
sciRefreshAlertVisible = NO;
|
||||
id strongSelf = weakSelf;
|
||||
if (!strongSelf) return;
|
||||
sciClearPreservedIds();
|
||||
sciRefreshConfirmInFlight = YES;
|
||||
((void(*)(id, SEL))objc_msgSend)(strongSelf, _cmd);
|
||||
sciRefreshConfirmInFlight = NO;
|
||||
}]];
|
||||
|
||||
sciRefreshAlertVisible = YES;
|
||||
UIViewController *top = [UIApplication sharedApplication].keyWindow.rootViewController;
|
||||
while (top.presentedViewController) top = top.presentedViewController;
|
||||
[top presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
%ctor {
|
||||
Class cls = NSClassFromString(@"IGDirectInboxViewController");
|
||||
if (!cls) return;
|
||||
SEL sel = NSSelectorFromString(@"_pullToRefreshIfPossible");
|
||||
if (class_getInstanceMethod(cls, sel))
|
||||
MSHookMessageEx(cls, sel, (IMP)new_pullToRefresh, (IMP *)&orig_pullToRefresh);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ static NSMutableSet *sciPreservedIds = nil;
|
||||
#define SCI_PRESERVED_MAX 200
|
||||
#define SCI_PRESERVED_TAG 1399
|
||||
|
||||
static NSMutableSet *sciGetPreservedIds() {
|
||||
NSMutableSet *sciGetPreservedIds() {
|
||||
if (!sciPreservedIds) {
|
||||
NSArray *saved = [[NSUserDefaults standardUserDefaults] arrayForKey:SCI_PRESERVED_IDS_KEY];
|
||||
sciPreservedIds = saved ? [NSMutableSet setWithArray:saved] : [NSMutableSet set];
|
||||
@@ -48,7 +48,7 @@ static void sciSavePreservedIds() {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:[ids allObjects] forKey:SCI_PRESERVED_IDS_KEY];
|
||||
}
|
||||
|
||||
static void sciClearPreservedIds() {
|
||||
void sciClearPreservedIds() {
|
||||
[sciGetPreservedIds() removeAllObjects];
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:SCI_PRESERVED_IDS_KEY];
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
BOOL dmSeenToggleEnabled = NO;
|
||||
static BOOL sciSeenAutoBypass = NO;
|
||||
__weak IGDirectThreadViewController *sciActiveThreadVC = nil;
|
||||
|
||||
static BOOL sciIsSeenToggleMode() {
|
||||
return [[SCIUtils getStringPref:@"seen_mode"] isEqualToString:@"toggle"];
|
||||
@@ -21,7 +22,11 @@ static BOOL sciAutoInteractEnabled() {
|
||||
return [SCIUtils getBoolPref:@"remove_lastseen"] && [SCIUtils getBoolPref:@"seen_auto_on_interact"];
|
||||
}
|
||||
|
||||
static void sciDoAutoSeen(IGDirectThreadViewController *threadVC) {
|
||||
BOOL sciAutoTypingEnabled() {
|
||||
return [SCIUtils getBoolPref:@"remove_lastseen"] && [SCIUtils getBoolPref:@"seen_auto_on_typing"];
|
||||
}
|
||||
|
||||
void sciDoAutoSeen(IGDirectThreadViewController *threadVC) {
|
||||
sciSeenAutoBypass = YES;
|
||||
[threadVC markLastMessageAsSeen];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
@@ -40,6 +45,21 @@ static void new_setHasSent(id self, SEL _cmd, BOOL sent) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============ AUTO SEEN ON TYPING ============
|
||||
// Tracks the visible thread VC so the typing-service hook (in
|
||||
// DisableTypingStatus.x) can mark its messages as seen.
|
||||
|
||||
%hook IGDirectThreadViewController
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
%orig;
|
||||
sciActiveThreadVC = self;
|
||||
}
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
if (sciActiveThreadVC == self) sciActiveThreadVC = nil;
|
||||
%orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// ============ NAV BAR BUTTONS ============
|
||||
|
||||
%hook IGTallNavigationBarView
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Saves to a dedicated "RyukGram" album in the Photos library.
|
||||
// Creates the album on first use. All RyukGram-initiated saves should go
|
||||
// through here so the user can find their downloads in one place.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
@interface SCIPhotoAlbum : NSObject
|
||||
|
||||
// Album name shown in the user's Photos app.
|
||||
+ (NSString *)albumName;
|
||||
|
||||
// Asynchronously fetches (or creates on first use) the RyukGram album.
|
||||
+ (void)fetchOrCreateAlbumWithCompletion:(void (^)(PHAssetCollection *album, NSError *error))completion;
|
||||
|
||||
// Saves a file at fileURL into the RyukGram album. The file is treated as a
|
||||
// photo or video based on its extension. Calls completion on the main queue.
|
||||
+ (void)saveFileToAlbum:(NSURL *)fileURL completion:(void (^)(BOOL success, NSError *error))completion;
|
||||
|
||||
// Watches the photo library for the next asset insertion and moves it into
|
||||
// the RyukGram album. Used to capture saves performed via UIActivityViewController's
|
||||
// "Save to Photos" activity, which we don't initiate ourselves.
|
||||
//
|
||||
// The watcher auto-unregisters after the first capture or after a timeout.
|
||||
+ (void)watchForNextSavedAsset;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,139 @@
|
||||
#import "PhotoAlbum.h"
|
||||
|
||||
@interface SCIPhotoAlbumWatcher : NSObject <PHPhotoLibraryChangeObserver>
|
||||
@property (nonatomic, strong) PHFetchResult<PHAsset *> *baseline;
|
||||
@property (nonatomic, strong) NSTimer *timeoutTimer;
|
||||
@end
|
||||
|
||||
static SCIPhotoAlbumWatcher *sciActiveWatcher = nil;
|
||||
|
||||
@implementation SCIPhotoAlbum
|
||||
|
||||
+ (NSString *)albumName {
|
||||
return @"RyukGram";
|
||||
}
|
||||
|
||||
+ (void)fetchOrCreateAlbumWithCompletion:(void (^)(PHAssetCollection *, NSError *))completion {
|
||||
PHFetchOptions *opts = [[PHFetchOptions alloc] init];
|
||||
opts.predicate = [NSPredicate predicateWithFormat:@"title = %@", [self albumName]];
|
||||
PHFetchResult<PHAssetCollection *> *result = [PHAssetCollection
|
||||
fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum
|
||||
subtype:PHAssetCollectionSubtypeAlbumRegular
|
||||
options:opts];
|
||||
if (result.count > 0) {
|
||||
if (completion) completion(result.firstObject, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
__block NSString *placeholderId = nil;
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||
PHAssetCollectionChangeRequest *req = [PHAssetCollectionChangeRequest
|
||||
creationRequestForAssetCollectionWithTitle:[self albumName]];
|
||||
placeholderId = req.placeholderForCreatedAssetCollection.localIdentifier;
|
||||
} completionHandler:^(BOOL success, NSError *error) {
|
||||
if (!success || !placeholderId) {
|
||||
if (completion) completion(nil, error);
|
||||
return;
|
||||
}
|
||||
PHFetchResult<PHAssetCollection *> *fetched = [PHAssetCollection
|
||||
fetchAssetCollectionsWithLocalIdentifiers:@[placeholderId] options:nil];
|
||||
if (completion) completion(fetched.firstObject, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)saveFileToAlbum:(NSURL *)fileURL completion:(void (^)(BOOL, NSError *))completion {
|
||||
[self fetchOrCreateAlbumWithCompletion:^(PHAssetCollection *album, NSError *err) {
|
||||
if (!album) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (completion) completion(NO, err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
__block NSString *assetId = nil;
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||
NSString *ext = [[fileURL pathExtension] lowercaseString];
|
||||
BOOL isVideo = [@[@"mp4", @"mov", @"m4v"] containsObject:ext];
|
||||
|
||||
PHAssetCreationRequest *req = [PHAssetCreationRequest creationRequestForAsset];
|
||||
PHAssetResourceCreationOptions *opts = [[PHAssetResourceCreationOptions alloc] init];
|
||||
opts.shouldMoveFile = YES;
|
||||
[req addResourceWithType:(isVideo ? PHAssetResourceTypeVideo : PHAssetResourceTypePhoto)
|
||||
fileURL:fileURL options:opts];
|
||||
req.creationDate = [NSDate date];
|
||||
assetId = req.placeholderForCreatedAsset.localIdentifier;
|
||||
|
||||
PHAssetCollectionChangeRequest *albumReq =
|
||||
[PHAssetCollectionChangeRequest changeRequestForAssetCollection:album];
|
||||
[albumReq addAssets:@[req.placeholderForCreatedAsset]];
|
||||
} completionHandler:^(BOOL success, NSError *changeErr) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (completion) completion(success, changeErr);
|
||||
});
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)watchForNextSavedAsset {
|
||||
// Replace any existing watcher — only the most recent share sheet matters
|
||||
if (sciActiveWatcher) {
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:sciActiveWatcher];
|
||||
[sciActiveWatcher.timeoutTimer invalidate];
|
||||
sciActiveWatcher = nil;
|
||||
}
|
||||
|
||||
if ([PHPhotoLibrary authorizationStatus] != PHAuthorizationStatusAuthorized &&
|
||||
[PHPhotoLibrary authorizationStatus] != PHAuthorizationStatusLimited) {
|
||||
return;
|
||||
}
|
||||
|
||||
SCIPhotoAlbumWatcher *watcher = [[SCIPhotoAlbumWatcher alloc] init];
|
||||
PHFetchOptions *opts = [[PHFetchOptions alloc] init];
|
||||
opts.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
|
||||
watcher.baseline = [PHAsset fetchAssetsWithOptions:opts];
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:watcher];
|
||||
|
||||
// Auto-unregister after 60s in case the user dismisses without saving
|
||||
watcher.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:60.0
|
||||
repeats:NO
|
||||
block:^(NSTimer *t) {
|
||||
if (sciActiveWatcher == watcher) {
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:watcher];
|
||||
sciActiveWatcher = nil;
|
||||
}
|
||||
}];
|
||||
sciActiveWatcher = watcher;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation SCIPhotoAlbumWatcher
|
||||
|
||||
- (void)photoLibraryDidChange:(PHChange *)changeInstance {
|
||||
PHFetchResultChangeDetails *details = [changeInstance changeDetailsForFetchResult:self.baseline];
|
||||
if (!details || details.insertedObjects.count == 0) return;
|
||||
|
||||
NSArray<PHAsset *> *inserted = details.insertedObjects;
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||
[SCIPhotoAlbum fetchOrCreateAlbumWithCompletion:^(PHAssetCollection *album, NSError *err) {}];
|
||||
} completionHandler:^(BOOL success, NSError *error) {
|
||||
// Add the inserted assets to the album in a separate transaction so the
|
||||
// album exists by the time we reference it.
|
||||
[SCIPhotoAlbum fetchOrCreateAlbumWithCompletion:^(PHAssetCollection *album, NSError *err) {
|
||||
if (!album) return;
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||
PHAssetCollectionChangeRequest *req =
|
||||
[PHAssetCollectionChangeRequest changeRequestForAssetCollection:album];
|
||||
[req addAssets:inserted];
|
||||
} completionHandler:nil];
|
||||
}];
|
||||
}];
|
||||
|
||||
// One-shot
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
|
||||
[self.timeoutTimer invalidate];
|
||||
self.timeoutTimer = nil;
|
||||
if (sciActiveWatcher == self) sciActiveWatcher = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -118,10 +118,12 @@
|
||||
},
|
||||
@{
|
||||
@"header": @"Download method",
|
||||
@"footer": @"When \"Save to RyukGram album\" is on, downloads and share-sheet \"Save to Photos\" picks are routed into a dedicated \"RyukGram\" album in your Photos library.",
|
||||
@"rows": @[
|
||||
[SCISetting menuCellWithTitle:@"Download method" subtitle:@"How to trigger downloads" menu:[self menus][@"dw_method"]],
|
||||
[SCISetting menuCellWithTitle:@"Save action" subtitle:@"What happens after downloading" menu:[self menus][@"dw_save_action"]],
|
||||
[SCISetting switchCellWithTitle:@"Confirm before download" subtitle:@"Show a confirmation dialog before starting a download" defaultsKey:@"dw_confirm"]
|
||||
[SCISetting switchCellWithTitle:@"Confirm before download" subtitle:@"Show a confirmation dialog before starting a download" defaultsKey:@"dw_confirm"],
|
||||
[SCISetting switchCellWithTitle:@"Save to RyukGram album" subtitle:@"Route saves into a dedicated album in Photos instead of the camera roll root" defaultsKey:@"save_to_ryukgram_album"]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@@ -144,11 +146,12 @@
|
||||
icon:nil
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"footer": @"Pull to refresh in the messages tab will remove preserved messages",
|
||||
@"footer": @"⚠️ WARNING: Pull-to-refresh in the DMs tab CLEARS all preserved messages. Enable \"Warn before clearing on refresh\" below to get a confirmation dialog before this happens.",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Keep deleted messages" subtitle:@"Preserves messages that others unsend" defaultsKey:@"keep_deleted_message"],
|
||||
[SCISetting switchCellWithTitle:@"Indicate unsent messages" subtitle:@"Shows an \"Unsent\" label on preserved messages" defaultsKey:@"indicate_unsent_messages"],
|
||||
[SCISetting switchCellWithTitle:@"Unsent message notification" subtitle:@"Shows a notification pill when a message is unsent" defaultsKey:@"unsent_message_toast"],
|
||||
[SCISetting switchCellWithTitle:@"Warn before clearing on refresh" subtitle:@"Show a confirmation dialog when pulling to refresh the DMs tab if preserved messages would be cleared" defaultsKey:@"warn_refresh_clears_preserved"],
|
||||
]
|
||||
}]
|
||||
],
|
||||
@@ -161,6 +164,7 @@
|
||||
[SCISetting switchCellWithTitle:@"Manually mark messages as seen" subtitle:@"Adds a button to DM threads to mark messages as seen" defaultsKey:@"remove_lastseen"],
|
||||
[SCISetting menuCellWithTitle:@"Read receipt mode" subtitle:@"How the seen button behaves" menu:[self menus][@"seen_mode"]],
|
||||
[SCISetting switchCellWithTitle:@"Auto mark seen on interact" subtitle:@"Locally marks messages as seen when you send any message" defaultsKey:@"seen_auto_on_interact"],
|
||||
[SCISetting switchCellWithTitle:@"Auto mark seen on typing" subtitle:@"Marks messages as seen the moment you start typing in a DM (works even when typing status is hidden)" defaultsKey:@"seen_auto_on_typing"],
|
||||
]
|
||||
}]
|
||||
],
|
||||
|
||||
+4
-1
@@ -48,11 +48,14 @@ BOOL dmVisualMsgsViewedButtonEnabled = false;
|
||||
@"no_seen_visual": @(YES),
|
||||
@"send_audio_as_file": @(YES),
|
||||
@"download_audio_message": @(NO),
|
||||
@"save_to_ryukgram_album": @(NO),
|
||||
@"unlock_password_reels": @(YES),
|
||||
@"seen_mode": @"button",
|
||||
@"seen_auto_on_interact": @(NO),
|
||||
@"seen_auto_on_typing": @(NO),
|
||||
@"indicate_unsent_messages": @(NO),
|
||||
@"unsent_message_toast": @(NO)
|
||||
@"unsent_message_toast": @(NO),
|
||||
@"warn_refresh_clears_preserved": @(NO)
|
||||
};
|
||||
[[NSUserDefaults standardUserDefaults] registerDefaults:sciDefaults];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#import "Utils.h"
|
||||
#import "PhotoAlbum.h"
|
||||
|
||||
@implementation SCIUtils
|
||||
|
||||
@@ -99,6 +100,13 @@
|
||||
acVC.popoverPresentationController.sourceView = topVC.view;
|
||||
acVC.popoverPresentationController.sourceRect = CGRectMake(topVC.view.bounds.size.width / 2.0, topVC.view.bounds.size.height / 2.0, 1.0, 1.0);
|
||||
}
|
||||
|
||||
// If the user picks "Save to Photos" from the share sheet, route the new
|
||||
// asset into the RyukGram album via a one-shot photo library observer.
|
||||
if ([self getBoolPref:@"save_to_ryukgram_album"]) {
|
||||
[SCIPhotoAlbum watchForNextSavedAsset];
|
||||
}
|
||||
|
||||
[topVC presentViewController:acVC animated:true completion:nil];
|
||||
}
|
||||
+ (void)showSettingsVC:(UIWindow *)window {
|
||||
|
||||
Reference in New Issue
Block a user