[release] RyukGram v1.2.0

### Features
- **Open Instagram links in app (Safari extension)** — bundled Safari web extension (sideload IPA only). Enable in Safari → Extensions; instagram.com links open in the app.
- **Localization** — every user-facing string flows through a central translation layer. Globe button in Settings; missing keys fall back to English. Ships English only — see the "Translating RyukGram" section in the README to add more.
- **Action buttons** — context-aware menus on feed, reels, and stories (expand, repost, download, copy caption, etc.) with per-context default tap action and carousel/multi-story bulk download
- **Enhanced HD downloads** — up to 1080p via DASH + FFmpegKit with quality picker, preview playback, encoding-speed options, and 720p fallback
- **Repost**, **media viewer**, **media zoom** (long-press), **download pill** (frosted glass, stacks concurrent downloads)
- **Fake location** — overrides CoreLocation app-wide, map picker + saved presets, optional quick-toggle button on the Friends Map
- **Messages-only mode** — strips every tab except DM inbox + profile
- **Launch tab** — pick which tab the app opens to
- Full last active date in DMs — show full date instead of "Active 2h ago"
- Custom date format — 12 formats with per-surface toggles (feed, notes/comments/stories, DMs)
- Send files in DMs (experimental)
- View story mentions
- Hide suggested stories
- Story tray long-press actions — view HD profile picture from the tray menu
- Advance on story reply — auto-skip to next story after sending a reply or reaction
- Mark story as seen on reply or emoji reaction
- Hide metrics (likes, comments, shares counts)
- Hide messages tab
- Hide voice/video call buttons in DM thread header (independent toggles)
- Disable app haptics
- Disable reels tab refresh
- Disable disappearing messages mode in DMs
- Follow indicator — shows whether the profile user follows you
- Copy note text on long press
- Zoom profile photo — long press opens full-screen viewer
- Notes actions — copy text, download GIF/audio from notes long-press menu
- Confirm unfollow
- Feed refresh controls — disable background refresh, home button refresh, and home button scroll

### Improvements
- Default tap action: added copy URL, repost, and view mentions options; dynamic menu generation per context
- Settings pages reordered: General → Feed → Stories → Reels → Messages → Profile → Navigation → Saving → Confirmations
- Fake location picker: native Apple Maps-style UI (search, long-press to drop pin, current location)
- Liquid glass floating tab bar + dynamic sizing
- Upload audio: FFmpegKit re-encode + trim for any audio/video input
- Settings reorganized with per-context action button config; new Profile page
- Highlight cover: full-screen viewer replaces direct download
- Switched HD encoder to `h264_videotoolbox` (hardware) — no GPL FFmpegKit required
- Legacy long-press download deprecated (off by default), replaced by action buttons

### Fixes
- Hide suggested stories no longer removes followed users' stories on scroll
- Settings search bar transparency with liquid glass off; auto-deactivates on push
- HD download cancel: tapping pill aborts in-flight downloads + FFmpeg sessions cleanly
- Download pill stuck state on background/foreground, progress reset per download
- Disappearing messages mode confirmation not firing on swipe
- Detailed color picker not working on story draw `†`
- DM seen toggle menu not updating after tap
- Reel refresh confirmation appearing on first app launch `†`
- Reels action button displacing profile pictures on photo reels
- Disappearing DM media download (expand, share, save to Photos with progress pill)
- Carousel "Download all" not showing item count in feed
- Encoding speed setting being ignored for HD downloads
- Various upstream SCInsta merges (Meta AI hiding, suggested chats hiding, notes tray) — marked `†`

> `†` Merged from upstream [SCInsta](https://github.com/SoCuul/SCInsta) by SoCuul

### Credits
- Thanks to [@erupts0](https://github.com/erupts0) (John) for testing and feature suggestions
- Thanks to [@euoradan](https://t.me/euoradan) (Radan) for experimental Instagram feature flag research
- Safari extension forked/cleaned from [BillyCurtis/OpenInstagramSafariExtension](https://github.com/BillyCurtis/OpenInstagramSafariExtension)

### Known Issues
- Preserved unsent messages can't be removed via "Delete for you"; pull-to-refresh clears them (warning available in settings)
- "Delete for you" detection uses a ~2s window after the local action — a real unsend landing in that window may be missed (rare)
This commit is contained in:
faroukbmiled
2026-04-16 03:03:42 +01:00
parent 86eaa95019
commit 51c1dc59cf
4 changed files with 174 additions and 186 deletions
+13 -2
View File
@@ -1,5 +1,6 @@
#import "SCIActionButton.h"
#import "SCIActionMenu.h"
#import "SCIRepostSheet.h"
#import "../Utils.h"
#import <objc/runtime.h>
@@ -123,8 +124,18 @@ const void *kSCIDismissKey = &kSCIDismissKey;
[SCIMediaActions downloadAndShareMedia:media];
} else if ([tap isEqualToString:@"download_photos"]) {
[SCIMediaActions downloadAndSaveMedia:media];
} else {
// Fallback: user can long-press for menu.
} else if ([tap isEqualToString:@"copy_link"]) {
[SCIMediaActions copyURLForMedia:media];
} else if ([tap isEqualToString:@"repost"]) {
NSURL *vidURL = [SCIUtils getVideoUrlForMedia:(id)media];
NSURL *imgURL = [SCIUtils getPhotoUrlForMedia:(id)media];
[SCIRepostSheet repostWithVideoURL:vidURL photoURL:imgURL];
} else if ([tap isEqualToString:@"view_mentions"]) {
UIViewController *host = [SCIUtils nearestViewControllerForView:sender];
if (host) {
extern void sciShowStoryMentions(UIViewController *, UIView *);
sciShowStoryMentions(host, sender);
}
}
}
+22 -43
View File
@@ -1,5 +1,5 @@
// Hide suggested stories from the tray. Drops items the user doesn't follow
// (friendship_status.following=0 or empty fieldCache); highlights pass through.
// Hide suggested stories from the tray. Only filters when suggested items
// are present — skips clean inputs to avoid IGListKit diff cascade.
#import "../../InstagramHeaders.h"
#import "../../Utils.h"
@@ -7,75 +7,58 @@
#import <objc/message.h>
#import <substrate.h>
// IGListAdapter declared in InstagramHeaders.h
static __weak id sciTrayAdapter = nil;
// ── Suggested item detection ──
// Returns YES if the item should be kept. Highlights / non-tray rows pass
// through; followed reels keep; empty fieldCache (freshly-streamed suggested
// users) drops; otherwise check friendship_status.following.
static BOOL sciIsFollowedTrayItem(id obj) {
if (![NSStringFromClass([obj class]) isEqualToString:@"IGStoryTrayViewModel"]) return YES;
static BOOL sciIsSuggestedTrayItem(id obj) {
if (![NSStringFromClass([obj class]) isEqualToString:@"IGStoryTrayViewModel"]) return NO;
@try {
if ([[obj valueForKey:@"isCurrentUserReel"] boolValue]) return YES;
if ([[obj valueForKey:@"isCurrentUserReel"] boolValue]) return NO;
id owner = [obj valueForKey:@"reelOwner"];
if (!owner) return YES;
if (!owner) return NO;
Ivar userIvar = class_getInstanceVariable([owner class], "_userReelOwner_user");
if (!userIvar) return YES;
if (!userIvar) return NO;
id igUser = object_getIvar(owner, userIvar);
if (!igUser) return YES;
if (!igUser) return NO;
Ivar fcIvar = NULL;
for (Class c = [igUser class]; c && !fcIvar; c = class_getSuperclass(c))
fcIvar = class_getInstanceVariable(c, "_fieldCache");
if (!fcIvar) return YES;
const char *fcType = ivar_getTypeEncoding(fcIvar);
if (!fcType || fcType[0] != '@') return YES;
if (!fcIvar) return NO;
id fc = object_getIvar(igUser, fcIvar);
if (![fc isKindOfClass:[NSDictionary class]]) return YES;
if ([(NSDictionary *)fc count] == 0) return NO;
if (![fc isKindOfClass:[NSDictionary class]]) return NO;
if ([(NSDictionary *)fc count] == 0) return YES;
id fs = [(NSDictionary *)fc objectForKey:@"friendship_status"];
if (!fs) return YES;
if (!fs) return NO;
return [[fs valueForKey:@"following"] boolValue];
return ![[fs valueForKey:@"following"] boolValue];
} @catch (__unused NSException *e) {
return YES;
return NO;
}
}
// ── Data source filter ──
static NSArray *(*orig_objectsForListAdapter)(id, SEL, id);
static NSArray *hook_objectsForListAdapter(id self, SEL _cmd, id adapter) {
NSArray *objects = orig_objectsForListAdapter(self, _cmd, adapter);
sciTrayAdapter = adapter;
if (![SCIUtils getBoolPref:@"hide_suggested_stories"]) return objects;
// Pass through unchanged when input has no suggestions (avoids cascade).
BOOL hasSuggested = NO;
for (id obj in objects) {
if (sciIsSuggestedTrayItem(obj)) { hasSuggested = YES; break; }
}
if (!hasSuggested) return objects;
NSMutableArray *filtered = [NSMutableArray arrayWithCapacity:objects.count];
for (id obj in objects) {
if (sciIsFollowedTrayItem(obj)) [filtered addObject:obj];
if (!sciIsSuggestedTrayItem(obj)) [filtered addObject:obj];
}
return [filtered copy];
}
// ── Reload tray on pref change ──
static void sciReloadTray(void) {
dispatch_async(dispatch_get_main_queue(), ^{
IGListAdapter *adapter = sciTrayAdapter;
if (adapter) [adapter performUpdatesAnimated:YES completion:nil];
});
}
%ctor {
Class dsCls = NSClassFromString(@"IGStoryTrayListAdapterDataSource");
if (!dsCls) return;
@@ -83,8 +66,4 @@ static void sciReloadTray(void) {
SEL sel = NSSelectorFromString(@"objectsForListAdapter:");
if (class_getInstanceMethod(dsCls, sel))
MSHookMessageEx(dsCls, sel, (IMP)hook_objectsForListAdapter, (IMP *)&orig_objectsForListAdapter);
[[NSNotificationCenter defaultCenter] addObserverForName:@"SCISuggestedStoriesReload"
object:nil queue:nil
usingBlock:^(NSNotification *n) { sciReloadTray(); }];
}
@@ -158,11 +158,8 @@ static void sciResumeStoryPlayback(UIView *sourceView) {
[btn.heightAnchor constraintEqualToConstant:36]
]];
[SCIActionButton configureButton:btn
context:SCIActionContextStories
prefKey:@"stories_action_default"
mediaProvider:^id (UIView *sourceView) {
// DM disappearing message — handle directly
SCIActionMediaProvider storyProvider = ^id (UIView *sourceView) {
// DM disappearing message — handle directly for tap actions
UIViewController *dmVC = sciFindVC(sourceView, @"IGDirectVisualMessageViewerController");
if (dmVC) {
sciDownloadDisappearingMedia(dmVC);
@@ -174,34 +171,40 @@ static void sciResumeStoryPlayback(UIView *sourceView) {
id item = sciGetCurrentStoryItem(sourceView);
if ([item isKindOfClass:NSClassFromString(@"IGMedia")]) return item;
return sciExtractMediaFromItem(item);
}];
};
// For DM visual messages: override menu with download/share/expand
btn.menu = [UIMenu menuWithChildren:@[
[UIDeferredMenuElement elementWithUncachedProvider:^(void (^completion)(NSArray<UIMenuElement *> *)) {
UIViewController *dmVC = sciFindVC(btn, @"IGDirectVisualMessageViewerController");
if (dmVC) {
completion(@[
[UIAction actionWithTitle:SCILocalized(@"Expand") image:[UIImage systemImageNamed:@"arrow.up.left.and.arrow.down.right"]
identifier:nil handler:^(UIAction *a) { sciExpandDisappearingMedia(dmVC); }],
[UIAction actionWithTitle:SCILocalized(@"Share") image:[UIImage systemImageNamed:@"square.and.arrow.up"]
identifier:nil handler:^(UIAction *a) { sciShareDisappearingMedia(dmVC); }],
[UIAction actionWithTitle:SCILocalized(@"Save to Photos") image:[UIImage systemImageNamed:@"square.and.arrow.down"]
identifier:nil handler:^(UIAction *a) { sciDownloadDisappearingMedia(dmVC); }],
]);
} else {
// Story — use normal action menu
id media = nil;
sciPauseStoryPlayback(btn);
id item = sciGetCurrentStoryItem(btn);
media = [item isKindOfClass:NSClassFromString(@"IGMedia")] ? item : sciExtractMediaFromItem(item);
NSArray *actions = [SCIMediaActions actionsForContext:SCIActionContextStories media:media fromView:btn];
UIMenu *built = [SCIActionMenu buildMenuWithActions:actions];
completion(built.children);
}
}]
]];
btn.showsMenuAsPrimaryAction = YES;
[SCIActionButton configureButton:btn
context:SCIActionContextStories
prefKey:@"stories_action_default"
mediaProvider:storyProvider];
// When configureButton chose "menu" mode, override with our custom
// deferred menu that handles both DM and story contexts.
if (btn.showsMenuAsPrimaryAction) {
btn.menu = [UIMenu menuWithChildren:@[
[UIDeferredMenuElement elementWithUncachedProvider:^(void (^completion)(NSArray<UIMenuElement *> *)) {
UIViewController *dmVC = sciFindVC(btn, @"IGDirectVisualMessageViewerController");
if (dmVC) {
completion(@[
[UIAction actionWithTitle:SCILocalized(@"Expand") image:[UIImage systemImageNamed:@"arrow.up.left.and.arrow.down.right"]
identifier:nil handler:^(UIAction *a) { sciExpandDisappearingMedia(dmVC); }],
[UIAction actionWithTitle:SCILocalized(@"Share") image:[UIImage systemImageNamed:@"square.and.arrow.up"]
identifier:nil handler:^(UIAction *a) { sciShareDisappearingMedia(dmVC); }],
[UIAction actionWithTitle:SCILocalized(@"Save to Photos") image:[UIImage systemImageNamed:@"square.and.arrow.down"]
identifier:nil handler:^(UIAction *a) { sciDownloadDisappearingMedia(dmVC); }],
]);
} else {
id media = nil;
sciPauseStoryPlayback(btn);
id item = sciGetCurrentStoryItem(btn);
media = [item isKindOfClass:NSClassFromString(@"IGMedia")] ? item : sciExtractMediaFromItem(item);
NSArray *actions = [SCIMediaActions actionsForContext:SCIActionContextStories media:media fromView:btn];
UIMenu *built = [SCIActionMenu buildMenuWithActions:actions];
completion(built.children);
}
}]
]];
}
// KVO highlighted → resume playback when menu dismisses.
[btn addObserver:self forKeyPath:@"highlighted"
+104 -109
View File
@@ -167,83 +167,6 @@
]
}]
],
[SCISetting navigationCellWithTitle:SCILocalized(@"Reels")
subtitle:@""
icon:[SCISymbol symbolWithName:@"film.stack"]
navSections:@[@{
@"header": SCILocalized(@"Action button"),
@"footer": SCILocalized(@"Adds a RyukGram action button above the reel sidebar with view-cover/download/share/copy/expand/repost entries. Tap opens the menu by default; change the tap behavior below."),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Show action button") subtitle:SCILocalized(@"Places a button above the like/comment/share column on each reel") defaultsKey:@"reels_action_button"],
[SCISetting menuCellWithTitle:SCILocalized(@"Default tap action") subtitle:SCILocalized(@"What happens on a single tap. Long-press always opens the full menu") menu:[self menus][@"reels_action_default"]],
]
},
@{
@"header": @"",
@"rows": @[
[SCISetting menuCellWithTitle:SCILocalized(@"Tap Controls") subtitle:SCILocalized(@"Change what happens when you tap on a reel") menu:[self menus][@"reels_tap_control"]],
[SCISetting switchCellWithTitle:SCILocalized(@"Always show progress scrubber") subtitle:SCILocalized(@"Forces the progress bar to appear on every reel") defaultsKey:@"reels_show_scrubber"],
[SCISetting switchCellWithTitle:SCILocalized(@"Disable auto-unmuting reels") subtitle:SCILocalized(@"Prevents reels from unmuting when the volume/silent button is pressed") defaultsKey:@"disable_auto_unmuting_reels" requiresRestart:YES],
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm reel refresh") subtitle:SCILocalized(@"Shows an alert when you trigger a reels refresh") defaultsKey:@"refresh_reel_confirm"],
[SCISetting switchCellWithTitle:SCILocalized(@"Disable tab button refresh") subtitle:SCILocalized(@"Tapping the Reels tab while on reels does nothing") defaultsKey:@"disable_reels_tab_refresh"],
[SCISetting switchCellWithTitle:SCILocalized(@"Unlock password-locked reels") subtitle:SCILocalized(@"Shows buttons to reveal and auto-fill the password on locked reels") defaultsKey:@"unlock_password_reels"],
]
},
@{
@"header": SCILocalized(@"Hiding"),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Hide reels header") subtitle:SCILocalized(@"Hides the top navigation bar when watching reels") defaultsKey:@"hide_reels_header"],
[SCISetting switchCellWithTitle:SCILocalized(@"Hide repost button") subtitle:SCILocalized(@"Hides the repost button on the reels sidebar") defaultsKey:@"hide_reels_repost" requiresRestart:YES]
]
},
@{
@"header": SCILocalized(@"Limits"),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Disable scrolling reels") subtitle:SCILocalized(@"Prevents reels from being scrolled to the next video") defaultsKey:@"disable_scrolling_reels" requiresRestart:YES],
[SCISetting switchCellWithTitle:SCILocalized(@"Prevent doom scrolling") subtitle:SCILocalized(@"Limits the amount of reels available to scroll at any given time, and prevents refreshing") defaultsKey:@"prevent_doom_scrolling"],
[SCISetting stepperCellWithTitle:SCILocalized(@"Doom scrolling limit") subtitle:SCILocalized(@"Only loads %@ %@") defaultsKey:@"doom_scrolling_reel_count" min:1 max:100 step:1 label:@"reels" singularLabel:@"reel"]
]
}]
],
[SCISetting navigationCellWithTitle:SCILocalized(@"Profile")
subtitle:@""
icon:[SCISymbol symbolWithName:@"person.crop.circle"]
navSections:@[@{
@"header": @"",
@"footer": SCILocalized(@"Long-press gestures on profile elements — kept separate from the per-feature action buttons."),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Zoom profile photo") subtitle:SCILocalized(@"Long press a profile picture to open it in full-screen with zoom, share, and save") defaultsKey:@"zoom_profile_photo"],
[SCISetting switchCellWithTitle:SCILocalized(@"Save profile picture") subtitle:SCILocalized(@"Long press to download directly (ignored when zoom is on)") defaultsKey:@"save_profile"],
[SCISetting switchCellWithTitle:SCILocalized(@"View highlight cover") subtitle:SCILocalized(@"Adds a view option to the highlight long-press menu to open the cover in full-screen") defaultsKey:@"download_highlight_cover"],
[SCISetting switchCellWithTitle:SCILocalized(@"Profile copy button") subtitle:SCILocalized(@"Adds a button next to the burger menu on profiles to copy username, name or bio") defaultsKey:@"profile_copy_button"],
[SCISetting switchCellWithTitle:SCILocalized(@"Follow indicator") subtitle:SCILocalized(@"Shows whether the profile user follows you") defaultsKey:@"follow_indicator"],
[SCISetting switchCellWithTitle:SCILocalized(@"Copy note on long press") subtitle:SCILocalized(@"Long press the note bubble on a profile to copy the text") defaultsKey:@"profile_note_copy"],
]
}]
],
[SCISetting navigationCellWithTitle:SCILocalized(@"Saving")
subtitle:@""
icon:[SCISymbol symbolWithName:@"tray.and.arrow.down"]
navSections:@[@{
@"header": SCILocalized(@"Downloads"),
@"footer": SCILocalized(@"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 switchCellWithTitle:SCILocalized(@"Confirm before download") subtitle:SCILocalized(@"Show a confirmation dialog before starting a download") defaultsKey:@"dw_confirm"],
[SCISetting switchCellWithTitle:SCILocalized(@"Save to RyukGram album") subtitle:SCILocalized(@"Route saves into a dedicated album in Photos instead of the camera roll root") defaultsKey:@"save_to_ryukgram_album"]
]
},
[self enhancedDownloadsSection],
@{
@"header": SCILocalized(@"Legacy long-press gesture"),
@"footer": SCILocalized(@"Deprecated. The RyukGram action button (configured per feature in Feed/Reels/Stories) is the new way to download media. Enable this master toggle only if you prefer the old multi-finger long-press directly on the media."),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Enable long-press gesture") subtitle:SCILocalized(@"Master toggle for the deprecated gesture workflow (off by default)") defaultsKey:@"dw_legacy_gesture"],
[SCISetting menuCellWithTitle:SCILocalized(@"Save action") subtitle:SCILocalized(@"What happens after the gesture downloads") menu:[self menus][@"dw_save_action"]],
[SCISetting stepperCellWithTitle:SCILocalized(@"Finger count for long-press") subtitle:SCILocalized(@"Downloads with %@ %@") defaultsKey:@"dw_finger_count" min:1 max:5 step:1 label:@"fingers" singularLabel:@"finger"],
[SCISetting stepperCellWithTitle:SCILocalized(@"Long-press hold time") subtitle:SCILocalized(@"Press finger(s) for %@ %@") defaultsKey:@"dw_finger_duration" min:0 max:10 step:0.25 label:@"sec" singularLabel:@"sec"]
]
}]
],
[SCISetting navigationCellWithTitle:SCILocalized(@"Stories")
subtitle:@""
icon:[SCISymbol symbolWithName:@"circle.dashed"]
@@ -317,6 +240,44 @@
]
}]
],
[SCISetting navigationCellWithTitle:SCILocalized(@"Reels")
subtitle:@""
icon:[SCISymbol symbolWithName:@"film.stack"]
navSections:@[@{
@"header": SCILocalized(@"Action button"),
@"footer": SCILocalized(@"Adds a RyukGram action button above the reel sidebar with view-cover/download/share/copy/expand/repost entries. Tap opens the menu by default; change the tap behavior below."),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Show action button") subtitle:SCILocalized(@"Places a button above the like/comment/share column on each reel") defaultsKey:@"reels_action_button"],
[SCISetting menuCellWithTitle:SCILocalized(@"Default tap action") subtitle:SCILocalized(@"What happens on a single tap. Long-press always opens the full menu") menu:[self menus][@"reels_action_default"]],
]
},
@{
@"header": @"",
@"rows": @[
[SCISetting menuCellWithTitle:SCILocalized(@"Tap Controls") subtitle:SCILocalized(@"Change what happens when you tap on a reel") menu:[self menus][@"reels_tap_control"]],
[SCISetting switchCellWithTitle:SCILocalized(@"Always show progress scrubber") subtitle:SCILocalized(@"Forces the progress bar to appear on every reel") defaultsKey:@"reels_show_scrubber"],
[SCISetting switchCellWithTitle:SCILocalized(@"Disable auto-unmuting reels") subtitle:SCILocalized(@"Prevents reels from unmuting when the volume/silent button is pressed") defaultsKey:@"disable_auto_unmuting_reels" requiresRestart:YES],
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm reel refresh") subtitle:SCILocalized(@"Shows an alert when you trigger a reels refresh") defaultsKey:@"refresh_reel_confirm"],
[SCISetting switchCellWithTitle:SCILocalized(@"Disable tab button refresh") subtitle:SCILocalized(@"Tapping the Reels tab while on reels does nothing") defaultsKey:@"disable_reels_tab_refresh"],
[SCISetting switchCellWithTitle:SCILocalized(@"Unlock password-locked reels") subtitle:SCILocalized(@"Shows buttons to reveal and auto-fill the password on locked reels") defaultsKey:@"unlock_password_reels"],
]
},
@{
@"header": SCILocalized(@"Hiding"),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Hide reels header") subtitle:SCILocalized(@"Hides the top navigation bar when watching reels") defaultsKey:@"hide_reels_header"],
[SCISetting switchCellWithTitle:SCILocalized(@"Hide repost button") subtitle:SCILocalized(@"Hides the repost button on the reels sidebar") defaultsKey:@"hide_reels_repost" requiresRestart:YES]
]
},
@{
@"header": SCILocalized(@"Limits"),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Disable scrolling reels") subtitle:SCILocalized(@"Prevents reels from being scrolled to the next video") defaultsKey:@"disable_scrolling_reels" requiresRestart:YES],
[SCISetting switchCellWithTitle:SCILocalized(@"Prevent doom scrolling") subtitle:SCILocalized(@"Limits the amount of reels available to scroll at any given time, and prevents refreshing") defaultsKey:@"prevent_doom_scrolling"],
[SCISetting stepperCellWithTitle:SCILocalized(@"Doom scrolling limit") subtitle:SCILocalized(@"Only loads %@ %@") defaultsKey:@"doom_scrolling_reel_count" min:1 max:100 step:1 label:@"reels" singularLabel:@"reel"]
]
}]
],
[SCISetting navigationCellWithTitle:SCILocalized(@"Messages")
subtitle:@""
icon:[SCISymbol symbolWithName:@"bubble.left.and.bubble.right"]
@@ -433,6 +394,22 @@
]
}]
],
[SCISetting navigationCellWithTitle:SCILocalized(@"Profile")
subtitle:@""
icon:[SCISymbol symbolWithName:@"person.crop.circle"]
navSections:@[@{
@"header": @"",
@"footer": SCILocalized(@"Long-press gestures on profile elements — kept separate from the per-feature action buttons."),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Zoom profile photo") subtitle:SCILocalized(@"Long press a profile picture to open it in full-screen with zoom, share, and save") defaultsKey:@"zoom_profile_photo"],
[SCISetting switchCellWithTitle:SCILocalized(@"Save profile picture") subtitle:SCILocalized(@"Long press to download directly (ignored when zoom is on)") defaultsKey:@"save_profile"],
[SCISetting switchCellWithTitle:SCILocalized(@"View highlight cover") subtitle:SCILocalized(@"Adds a view option to the highlight long-press menu to open the cover in full-screen") defaultsKey:@"download_highlight_cover"],
[SCISetting switchCellWithTitle:SCILocalized(@"Profile copy button") subtitle:SCILocalized(@"Adds a button next to the burger menu on profiles to copy username, name or bio") defaultsKey:@"profile_copy_button"],
[SCISetting switchCellWithTitle:SCILocalized(@"Follow indicator") subtitle:SCILocalized(@"Shows whether the profile user follows you") defaultsKey:@"follow_indicator"],
[SCISetting switchCellWithTitle:SCILocalized(@"Copy note on long press") subtitle:SCILocalized(@"Long press the note bubble on a profile to copy the text") defaultsKey:@"profile_note_copy"],
]
}]
],
[SCISetting navigationCellWithTitle:SCILocalized(@"Navigation")
subtitle:@""
icon:[SCISymbol symbolWithName:@"hand.draw.fill"]
@@ -462,6 +439,29 @@
]
}]
],
[SCISetting navigationCellWithTitle:SCILocalized(@"Saving")
subtitle:@""
icon:[SCISymbol symbolWithName:@"tray.and.arrow.down"]
navSections:@[@{
@"header": SCILocalized(@"Downloads"),
@"footer": SCILocalized(@"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 switchCellWithTitle:SCILocalized(@"Confirm before download") subtitle:SCILocalized(@"Show a confirmation dialog before starting a download") defaultsKey:@"dw_confirm"],
[SCISetting switchCellWithTitle:SCILocalized(@"Save to RyukGram album") subtitle:SCILocalized(@"Route saves into a dedicated album in Photos instead of the camera roll root") defaultsKey:@"save_to_ryukgram_album"]
]
},
[self enhancedDownloadsSection],
@{
@"header": SCILocalized(@"Legacy long-press gesture"),
@"footer": SCILocalized(@"Deprecated. The RyukGram action button (configured per feature in Feed/Reels/Stories) is the new way to download media. Enable this master toggle only if you prefer the old multi-finger long-press directly on the media."),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Enable long-press gesture") subtitle:SCILocalized(@"Master toggle for the deprecated gesture workflow (off by default)") defaultsKey:@"dw_legacy_gesture"],
[SCISetting menuCellWithTitle:SCILocalized(@"Save action") subtitle:SCILocalized(@"What happens after the gesture downloads") menu:[self menus][@"dw_save_action"]],
[SCISetting stepperCellWithTitle:SCILocalized(@"Finger count for long-press") subtitle:SCILocalized(@"Downloads with %@ %@") defaultsKey:@"dw_finger_count" min:1 max:5 step:1 label:@"fingers" singularLabel:@"finger"],
[SCISetting stepperCellWithTitle:SCILocalized(@"Long-press hold time") subtitle:SCILocalized(@"Press finger(s) for %@ %@") defaultsKey:@"dw_finger_duration" min:0 max:10 step:0.25 label:@"sec" singularLabel:@"sec"]
]
}]
],
[SCISetting navigationCellWithTitle:SCILocalized(@"Confirm actions")
subtitle:@""
icon:[SCISymbol symbolWithName:@"checkmark"]
@@ -699,6 +699,30 @@
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
// Builds the default-tap-action picker menu for a given action button context.
// Adding a new tap action = one entry here. Order: actions first, downloads last.
+ (UIMenu *)defaultTapMenuForKey:(NSString *)key context:(NSString *)ctx {
// { value, title, contexts ("all" or csv of feed,reels,stories) }
NSArray *entries = @[
@[@"menu", SCILocalized(@"Open menu"), @"all"],
@[@"expand", SCILocalized(@"Expand"), @"all"],
@[@"repost", SCILocalized(@"Repost"), @"all"],
@[@"view_mentions", SCILocalized(@"View mentions"), @"stories"],
@[@"copy_link", SCILocalized(@"Copy download URL"), @"all"],
@[@"download_share", SCILocalized(@"Download and share"), @"all"],
@[@"download_photos",SCILocalized(@"Download to Photos"), @"all"],
];
NSMutableArray *children = [NSMutableArray array];
for (NSArray *e in entries) {
NSString *contexts = e[2];
if (![contexts isEqualToString:@"all"] && ![contexts containsString:ctx]) continue;
[children addObject:[UICommand commandWithTitle:e[1] image:nil
action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": key, @"value": e[0]}]];
}
return [UIMenu menuWithChildren:children];
}
+ (NSDictionary *)menus {
return @{
@"chat_blocking_mode": [UIMenu menuWithChildren:@[
@@ -784,38 +808,9 @@
]
]],
// Per-context action button default tap mode. Each feature page gets
// its own key so users can pick different defaults per context.
@"feed_action_default": [UIMenu menuWithChildren:@[
[UICommand commandWithTitle:SCILocalized(@"Open menu") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"feed_action_default", @"value": @"menu"}],
[UICommand commandWithTitle:SCILocalized(@"Expand") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"feed_action_default", @"value": @"expand"}],
[UICommand commandWithTitle:SCILocalized(@"Download and share") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"feed_action_default", @"value": @"download_share"}],
[UICommand commandWithTitle:SCILocalized(@"Download to Photos") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"feed_action_default", @"value": @"download_photos"}],
]],
@"reels_action_default": [UIMenu menuWithChildren:@[
[UICommand commandWithTitle:SCILocalized(@"Open menu") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"reels_action_default", @"value": @"menu"}],
[UICommand commandWithTitle:SCILocalized(@"Expand") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"reels_action_default", @"value": @"expand"}],
[UICommand commandWithTitle:SCILocalized(@"Download and share") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"reels_action_default", @"value": @"download_share"}],
[UICommand commandWithTitle:SCILocalized(@"Download to Photos") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"reels_action_default", @"value": @"download_photos"}],
]],
@"stories_action_default": [UIMenu menuWithChildren:@[
[UICommand commandWithTitle:SCILocalized(@"Open menu") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"stories_action_default", @"value": @"menu"}],
[UICommand commandWithTitle:SCILocalized(@"Expand") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"stories_action_default", @"value": @"expand"}],
[UICommand commandWithTitle:SCILocalized(@"Download and share") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"stories_action_default", @"value": @"download_share"}],
[UICommand commandWithTitle:SCILocalized(@"Download to Photos") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"stories_action_default", @"value": @"download_photos"}],
]],
@"feed_action_default": [self defaultTapMenuForKey:@"feed_action_default" context:@"feed"],
@"reels_action_default": [self defaultTapMenuForKey:@"reels_action_default" context:@"reels"],
@"stories_action_default": [self defaultTapMenuForKey:@"stories_action_default" context:@"stories"],
@"default_video_quality": [UIMenu menuWithChildren:@[
[UICommand commandWithTitle:SCILocalized(@"Always ask") image:nil action:@selector(menuChanged:)