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:
faroukbmiled
2026-04-07 03:04:50 +01:00
parent e634359acc
commit 6e96140895
12 changed files with 379 additions and 34 deletions
+20 -20
View File
@@ -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];
}
+21 -1
View File
@@ -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
+26
View File
@@ -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
+139
View File
@@ -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
+6 -2
View File
@@ -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
View File
@@ -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];
+8
View File
@@ -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 {