Files
RyukGram/src/Features/ActionButton/ReelsActionButton.xm
T
faroukbmiled 2977873932 [release] RyukGram v1.2.2
- Profile Analyzer (beta) — follower/following scans with mutuals, non-followbacks, new/lost trackers, and profile change history; searchable lists with batch follow/unfollow
- Theme settings — force dark mode, Full OLED, OLED chat theme, and keyboard theme picker
- Confirm story like
- Confirm story emoji reaction
- Swipe down to dismiss media viewer
- Manually add users to story/chat exclusion lists by username
- Keep stories visually seen locally
- Auto-scroll reels mode
- Quality picker: audio-only and raw photo download rows
- Clear cache button with optional auto-clear interval
- Spanish, Russian, Korean, Arabic, and Chinese (Traditional) translations
- About page with version, credits, and links
- Release notes popup on first launch of a new version
- Anonymous live viewing
- Toggle live comments
- Disappearing DM media overlay — action button, mark-as-viewed eye, and audio toggle
- Hide RyukGram UI on screenshots, screen recordings, and mirroring
- Open link from clipboard — long-press the search tab
- Messages-only mode: optional "Hide tab bar" sub-toggle
- Fake profile stats — verified badge and follower/following/post counts on your own profile
- Language switcher + import/export localization from Debug
- Reveal poll/slider vote counts and quiz answers on stories and reels before interacting
- Force legacy Quiz sticker back into the story composer tray
- Advanced experimental features menu — toggle hidden IG experiments (QuickSnap, Homecoming, Prism, Direct Notes reply types) with apply-on-restart batching and a crash-loop auto-reset
- Shortcut to Advanced experimental features from the General experimental features section

- Push notifications render with rich previews on sideload again
- IG 426 compatibility across story audio toggle, like confirmation, seen-on-like, live comments, notes audio download
- Call confirm split into separate voice-call and video-call toggles
- Messages-only mode: tab swiping disabled
- Settings quick-access broken in non-English languages
- Story seen-receipt block restored on IG v426
- Block selected mode no longer marks listed stories as seen
- Hide explore posts grid works again on recent IG versions
- Hide suggested stories no longer breaks profile highlights
- Hide trending searches now also hides the category chip bar
- Story eye long-press menu opens next to the button
- Disable video autoplay: tap-to-play now works on videos inside carousels
- Disable vanish mode swipe fixed on IG 426

- "Confirm shh mode" renamed to "Confirm vanish mode" across all languages
- Confirm sticker interaction split into separate story and highlight toggles
- Shared link embed presets: added eeinstagram.com and vxinstagram.com
- Downloaded media filenames follow `@username_context_timestamp`
- Reels pause mode: optional tap-to-mute on photo reels
- Backup & Restore — scope picker with live preview for Settings / Excluded lists / Analyzer data
- Profile Analyzer: filter by Not verified
- Settings header: tap to open a sheet with GitHub and Telegram channel links

- Thanks to Furamako for the Spanish translation
- Thanks to [ZomkaDEV](https://github.com/ZomkaDEV) for the Russian translation
- Thanks to [@ch1tmdgus](https://github.com/ch1tmdgus) (N4C) for the Korean translation
- Thanks to [@bruuhim](https://github.com/bruuhim) for the Arabic translation
- Thanks to [@jaydenjcpy](https://github.com/jaydenjcpy) for the Chinese (Traditional) translation
- Thanks to [@darthplagueiswise](https://github.com/darthplagueiswise) (Radan) for the experimental flag feature set
- Thanks to [@asdfzxcvbn](https://github.com/asdfzxcvbn) for [zxPluginsInject](https://github.com/asdfzxcvbn/zxPluginsInject) and [ipapatch](https://github.com/asdfzxcvbn/ipapatch)

- 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)
- With Liquid Glass buttons + Hide UI on capture both on, the DM eye leaves an empty glass bubble in captures
2026-04-24 02:50:30 +01:00

178 lines
6.7 KiB
Plaintext

// Reels action button — injects a RyukGram action button above the reel's
// vertical like/comment/share sidebar (IGSundialViewerVerticalUFI).
#import "../../InstagramHeaders.h"
#import "../../Utils.h"
#import "../../SCIChrome.h"
#import "../../ActionButton/SCIActionButton.h"
#import "../../ActionButton/SCIMediaActions.h"
#import <objc/runtime.h>
#import <objc/message.h>
static const NSInteger kReelActionBtnTag = 1337;
static UIView *sciFindSuperviewOfClass(UIView *view, NSString *className) {
Class cls = NSClassFromString(className);
if (!cls) return nil;
UIView *current = view.superview;
for (int depth = 0; current && depth < 20; depth++) {
if ([current isKindOfClass:cls]) return current;
current = current.superview;
}
return nil;
}
static id sciFindMediaIvar(UIView *view) {
if (!view) return nil;
Class mediaClass = NSClassFromString(@"IGMedia");
if (!mediaClass) return nil;
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([view class], &count);
id found = nil;
for (unsigned int i = 0; i < count; i++) {
const char *type = ivar_getTypeEncoding(ivars[i]);
if (!type || type[0] != '@') continue;
@try {
id val = object_getIvar(view, ivars[i]);
if (val && [val isKindOfClass:mediaClass]) { found = val; break; }
} @catch (__unused id e) {}
}
if (ivars) free(ivars);
return found;
}
// Resolve the current carousel child from _currentIndex.
static id sciCurrentCarouselChildMedia(UIView *carouselCell, id parentMedia) {
if (!carouselCell || !parentMedia) return parentMedia;
// Try _currentIndex ivar
Ivar idxIvar = class_getInstanceVariable([carouselCell class], "_currentIndex");
NSInteger currentIdx = 0;
if (idxIvar) {
ptrdiff_t offset = ivar_getOffset(idxIvar);
currentIdx = *(NSInteger *)((char *)(__bridge void *)carouselCell + offset);
}
// Fallback: _currentFractionalIndex
if (!idxIvar || currentIdx == 0) {
Ivar fracIvar = class_getInstanceVariable([carouselCell class], "_currentFractionalIndex");
if (fracIvar) {
ptrdiff_t fOffset = ivar_getOffset(fracIvar);
double fracIdx = *(double *)((char *)(__bridge void *)carouselCell + fOffset);
NSInteger roundedIdx = (NSInteger)round(fracIdx);
if (roundedIdx > 0) currentIdx = roundedIdx;
}
}
// Fallback: inner collection view content offset
Ivar cvIvar = class_getInstanceVariable([carouselCell class], "_collectionView");
if (cvIvar) {
UICollectionView *cv = object_getIvar(carouselCell, cvIvar);
if (cv) {
CGFloat pageWidth = cv.bounds.size.width;
if (pageWidth > 0) {
NSInteger cvIdx = (NSInteger)round(cv.contentOffset.x / pageWidth);
if (cvIdx > currentIdx) currentIdx = cvIdx;
}
}
}
NSArray *children = [SCIMediaActions carouselChildrenForMedia:parentMedia];
if (currentIdx >= 0 && (NSUInteger)currentIdx < children.count) {
return children[currentIdx];
}
return parentMedia;
}
// Media provider for reels. Returns current page's child for carousels.
static id sciReelsMediaProvider(UIView *sourceView) {
// Video reel
UIView *videoCell = sciFindSuperviewOfClass(sourceView, @"IGSundialViewerVideoCell");
if (videoCell) {
id m = sciFindMediaIvar(videoCell);
if (m) return m;
}
// Photo reel
UIView *photoCell = sciFindSuperviewOfClass(sourceView, @"IGSundialViewerPhotoCell");
if (photoCell) {
id m = sciFindMediaIvar(photoCell);
if (m) return m;
}
// Carousel reel
UIView *carouselCell = sciFindSuperviewOfClass(sourceView, @"IGSundialViewerCarouselCell");
if (carouselCell) {
id parentMedia = sciFindMediaIvar(carouselCell);
if (parentMedia) {
return sciCurrentCarouselChildMedia(carouselCell, parentMedia);
}
}
return nil;
}
%hook IGSundialViewerVerticalUFI
- (void)didMoveToSuperview {
%orig;
if (![SCIUtils getBoolPref:@"reels_action_button"]) return;
if (!self.superview) return;
SCIChromeButton *btn = (SCIChromeButton *)[self viewWithTag:kReelActionBtnTag];
if (![btn isKindOfClass:[SCIChromeButton class]]) btn = nil;
if (!btn) {
UIImageSymbolConfiguration *symCfg =
[UIImageSymbolConfiguration configurationWithPointSize:24 weight:UIImageSymbolWeightSemibold];
UIImage *base = [UIImage systemImageNamed:@"ellipsis.circle" withConfiguration:symCfg];
// Bake the drop shadow into the image so no CALayer shadow is needed.
CGFloat pad = 8;
CGSize sz = CGSizeMake(base.size.width + pad * 2, base.size.height + pad * 2);
UIGraphicsImageRenderer *r = [[UIGraphicsImageRenderer alloc] initWithSize:sz];
UIImage *icon = [r imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
CGContextRef c = ctx.CGContext;
CGContextSaveGState(c);
CGContextSetShadowWithColor(c, CGSizeMake(0, 1), 3,
[UIColor colorWithWhite:0 alpha:0.55].CGColor);
UIImage *tinted = [base imageWithTintColor:[UIColor whiteColor]
renderingMode:UIImageRenderingModeAlwaysOriginal];
[tinted drawInRect:CGRectMake(pad, pad, base.size.width, base.size.height)];
CGContextRestoreGState(c);
}];
btn = [[SCIChromeButton alloc] initWithSymbol:@"" pointSize:0 diameter:40];
btn.tag = kReelActionBtnTag;
btn.bubbleColor = [UIColor clearColor];
btn.iconView.image = icon;
// Capsule configuration gives us the native dark platter animation
// when the menu opens/closes — behaviour parity with IG's own chrome.
UIButtonConfiguration *cfg = [UIButtonConfiguration plainButtonConfiguration];
cfg.cornerStyle = UIButtonConfigurationCornerStyleCapsule;
cfg.background.backgroundColor = [UIColor clearColor];
cfg.contentInsets = NSDirectionalEdgeInsetsZero;
btn.configuration = cfg;
self.clipsToBounds = NO;
[self addSubview:btn];
[NSLayoutConstraint activateConstraints:@[
[btn.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[btn.bottomAnchor constraintEqualToAnchor:self.topAnchor constant:-10],
[btn.widthAnchor constraintEqualToConstant:40],
[btn.heightAnchor constraintEqualToConstant:40]
]];
}
[SCIActionButton configureButton:btn
context:SCIActionContextReels
prefKey:@"reels_action_default"
mediaProvider:^id (UIView *sourceView) {
return sciReelsMediaProvider(sourceView);
}];
}
%end