[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
+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"