feat: Mark seen on story like

feat: Added copy button in profile page to copy various profile information
feat: Added export/import settings option - With Searchable, collapsible, editable preview before saving or applying
imp: Search bar in tweak settings
imp: Hide custom story buttons when zooming (follows ig buttons)
bug: Fix a bug in keep deleted messages marking removed reactions as unset messages
This commit is contained in:
faroukbmiled
2026-04-08 03:19:05 +01:00
parent 6e96140895
commit 0643f5e691
11 changed files with 1522 additions and 38 deletions
+241
View File
@@ -0,0 +1,241 @@
#import "../../Utils.h"
#import "../../InstagramHeaders.h"
#import "../../../modules/JGProgressHUD/JGProgressHUD.h"
#import <objc/runtime.h>
#import <substrate.h>
// Profile page copy button: hooks IG's native nav header builder to insert
// a copy button alongside IG's own buttons, then opens a menu to copy
// username/name/bio.
@interface IGProfileViewController : UIViewController
@end
static id sci_safeValueForKey(id obj, NSString *key) {
@try { return [obj valueForKey:key]; }
@catch (__unused NSException *e) { return nil; }
}
static id sci_valueForAnyKey(id obj, NSArray<NSString *> *keys) {
for (NSString *k in keys) {
id v = sci_safeValueForKey(obj, k);
if (v && v != [NSNull null]) return v;
}
return nil;
}
static id sci_findUserOnVC(UIViewController *vc) {
id user = sci_valueForAnyKey(vc, @[@"user", @"userGQL", @"profileUser", @"loggedInUser", @"currentUser"]);
if (user) return user;
Class userCls = NSClassFromString(@"IGUser");
Class c = [vc class];
while (c && c != [NSObject class]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(c, &count);
for (unsigned int i = 0; i < count; i++) {
id v = object_getIvar(vc, ivars[i]);
if (userCls && [v isKindOfClass:userCls]) {
free(ivars);
return v;
}
}
if (ivars) free(ivars);
c = class_getSuperclass(c);
}
return nil;
}
static UIViewController *sci_findProfileVC(UIView *view) {
Class profileCls = NSClassFromString(@"IGProfileViewController");
UIResponder *r = view;
while (r) {
if (profileCls && [r isKindOfClass:profileCls]) return (UIViewController *)r;
r = [r nextResponder];
}
return nil;
}
static void sci_copyAndToast(NSString *value, NSString *label) {
if (value.length == 0) return;
[UIPasteboard generalPasteboard].string = value;
JGProgressHUD *HUD = [[JGProgressHUD alloc] init];
HUD.textLabel.text = [NSString stringWithFormat:@"Copied %@", label];
HUD.indicatorView = [[JGProgressHUDSuccessIndicatorView alloc] init];
UIView *host = nil;
for (UIWindow *w in [UIApplication sharedApplication].windows) {
if (w.isKeyWindow) { host = w; break; }
}
if (host) {
[HUD showInView:host];
[HUD dismissAfterDelay:1.5];
}
}
// Singleton target for the copy button so we don't have to track lifetime.
@interface SCIProfileCopyTarget : NSObject
+ (instancetype)shared;
- (void)handleTap:(UIButton *)sender;
@end
@implementation SCIProfileCopyTarget
+ (instancetype)shared {
static SCIProfileCopyTarget *s;
static dispatch_once_t once;
dispatch_once(&once, ^{ s = [[SCIProfileCopyTarget alloc] init]; });
return s;
}
- (void)handleTap:(UIButton *)sender {
UIViewController *vc = sci_findProfileVC(sender);
if (!vc) {
NSLog(@"[SCInsta] copy button: no IGProfileViewController in responder chain");
return;
}
id user = sci_findUserOnVC(vc);
if (!user) {
NSLog(@"[SCInsta] copy button: no IGUser found on %@", vc.class);
return;
}
NSString *username = [sci_valueForAnyKey(user, @[@"username"]) description];
NSString *fullName = [sci_valueForAnyKey(user, @[@"fullName", @"fullname", @"name"]) description];
NSString *biography = [sci_valueForAnyKey(user, @[@"biography", @"bio", @"profileBiography"]) description];
NSLog(@"[SCInsta] copy button user=%@ name=%@ bioLen=%lu",
username, fullName, (unsigned long)biography.length);
UIAlertController *menu = [UIAlertController alertControllerWithTitle:@"Copy from profile"
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
if (username.length) {
[menu addAction:[UIAlertAction actionWithTitle:[NSString stringWithFormat:@"Copy username (@%@)", username]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_) { sci_copyAndToast(username, @"username"); }]];
}
if (fullName.length) {
[menu addAction:[UIAlertAction actionWithTitle:@"Copy name"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_) { sci_copyAndToast(fullName, @"name"); }]];
}
if (biography.length) {
[menu addAction:[UIAlertAction actionWithTitle:@"Copy bio"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_) { sci_copyAndToast(biography, @"bio"); }]];
}
NSMutableArray *parts = [NSMutableArray array];
if (username.length) [parts addObject:[NSString stringWithFormat:@"Username: @%@", username]];
if (fullName.length) [parts addObject:[NSString stringWithFormat:@"Name: %@", fullName]];
if (biography.length) [parts addObject:[NSString stringWithFormat:@"Bio:\n%@", biography]];
if (parts.count >= 2) {
NSString *combined = [parts componentsJoinedByString:@"\n\n"];
[menu addAction:[UIAlertAction actionWithTitle:@"Copy all"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_) { sci_copyAndToast(combined, @"all"); }]];
}
if (menu.actions.count == 0) {
[menu addAction:[UIAlertAction actionWithTitle:@"Nothing to copy" style:UIAlertActionStyleDefault handler:nil]];
}
[menu addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
if (sender) {
menu.popoverPresentationController.sourceView = sender;
menu.popoverPresentationController.sourceRect = sender.bounds;
}
[vc presentViewController:menu animated:YES completion:nil];
}
@end
static UIView *sci_buildCopyButton(void) {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.accessibilityIdentifier = @"sci-profile-copy-button";
btn.accessibilityLabel = @"Copy profile info";
UIImageSymbolConfiguration *cfg =
[UIImageSymbolConfiguration configurationWithPointSize:16
weight:UIImageSymbolWeightRegular];
UIImage *icon = [[UIImage systemImageNamed:@"doc.on.doc"] imageByApplyingSymbolConfiguration:cfg];
[btn setImage:icon forState:UIControlStateNormal];
btn.tintColor = [UIColor labelColor];
btn.frame = CGRectMake(0, 0, 24, 44);
[btn addTarget:[SCIProfileCopyTarget shared]
action:@selector(handleTap:)
forControlEvents:UIControlEventTouchUpInside];
return btn;
}
static void (*orig_configureHeaderView)(id, SEL, id, id, id, BOOL);
static void hooked_configureHeaderView(id self, SEL _cmd,
id titleView,
id leftButtons,
id rightButtons,
BOOL titleIsCentered) {
if (![SCIUtils getBoolPref:@"profile_copy_button"]) {
orig_configureHeaderView(self, _cmd, titleView, leftButtons, rightButtons, titleIsCentered);
return;
}
NSArray *patched = rightButtons;
if ([rightButtons isKindOfClass:[NSArray class]] && [(NSArray *)rightButtons count] > 0) {
NSArray *rb = (NSArray *)rightButtons;
BOOL alreadyHas = NO;
for (id wrapper in rb) {
UIView *v = sci_safeValueForKey(wrapper, @"view");
if ([v isKindOfClass:[UIView class]] &&
[v.accessibilityIdentifier isEqualToString:@"sci-profile-copy-button"]) {
alreadyHas = YES;
break;
}
}
if (!alreadyHas) {
Class wrapperCls = NSClassFromString(@"IGProfileNavigationHeaderViewButtonSwift.IGProfileNavigationHeaderViewButton");
// Mirror an existing button's type so IG's layout treats ours the same.
NSInteger type = 0;
id sample = rb.firstObject;
id typeVal = sci_safeValueForKey(sample, @"type");
if ([typeVal respondsToSelector:@selector(integerValue)]) {
type = [typeVal integerValue];
}
UIView *btn = sci_buildCopyButton();
id wrapper = nil;
if (wrapperCls) {
wrapper = [wrapperCls alloc];
SEL initSel = @selector(initWithType:view:);
if ([wrapper respondsToSelector:initSel]) {
id (*ctor)(id, SEL, NSInteger, id) =
(id (*)(id, SEL, NSInteger, id))objc_msgSend;
wrapper = ctor(wrapper, initSel, type, btn);
}
}
if (wrapper) {
NSMutableArray *m = [rb mutableCopy];
[m insertObject:wrapper atIndex:0];
patched = m;
}
}
}
orig_configureHeaderView(self, _cmd, titleView, leftButtons, patched, titleIsCentered);
}
%ctor {
Class cls = objc_getClass("IGProfileNavigationSwift.IGProfileNavigationHeaderView");
if (!cls) return;
SEL sel = @selector(configureWithTitleView:leftButtons:rightButtons:titleIsCentered:);
if (![cls instancesRespondToSelector:sel]) return;
MSHookMessageEx(cls, sel,
(IMP)hooked_configureHeaderView,
(IMP *)&orig_configureHeaderView);
}
@@ -1,5 +1,8 @@
// Story seen receipt blocking + visual seen state blocking
#import "StoryHelpers.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <substrate.h>
BOOL sciSeenBypassActive = NO;
NSMutableSet *sciAllowedSeenPKs = nil;
@@ -92,3 +95,99 @@ static BOOL sciShouldBlockSeenVisual() {
- (void)setSeen:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; }
- (void)updateRingForSeenState:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; }
%end
// ============ MARK SEEN ON STORY LIKE ============
// Story likes are dispatched through several classes — IGSundialViewerControlsOverlayController,
// IGStoryLikesInteractionControllingImpl, and IGSundialLikeButton. Hook every
// known entry so any UI path (heart button, future double-tap, etc.) is caught.
// On a like, we defer to the manual seen-button handler in OverlayButtons.xm
// (sciMarkSeenTapped:) which is the only flow IG actually treats as a real seen.
static __weak UIViewController *sciActiveStoryVC = nil;
%hook IGStoryViewerViewController
- (void)viewDidAppear:(BOOL)animated {
%orig;
sciActiveStoryVC = self;
}
- (void)viewWillDisappear:(BOOL)animated {
if (sciActiveStoryVC == (UIViewController *)self) sciActiveStoryVC = nil;
%orig;
}
%end
static UIView *sciFindStoryOverlayView(UIViewController *vc) {
if (!vc) return nil;
Class targetCls = NSClassFromString(@"IGStoryFullscreenOverlayView");
if (!targetCls) return nil;
NSMutableArray *stack = [NSMutableArray arrayWithObject:vc.view];
while (stack.count > 0) {
UIView *v = stack.lastObject;
[stack removeLastObject];
if ([v isKindOfClass:targetCls]) return v;
for (UIView *sub in v.subviews) [stack addObject:sub];
}
return nil;
}
static void sciMarkActiveStorySeen(void) {
if (![SCIUtils getBoolPref:@"seen_on_story_like"]) return;
UIView *overlay = sciFindStoryOverlayView(sciActiveStoryVC);
if (!overlay) return;
SEL sel = NSSelectorFromString(@"sciMarkSeenTapped:");
if ([overlay respondsToSelector:sel]) {
((void(*)(id, SEL, id))objc_msgSend)(overlay, sel, nil);
}
}
static void (*orig_didLikeSundial)(id, SEL, id);
static void new_didLikeSundial(id self, SEL _cmd, id pk) {
orig_didLikeSundial(self, _cmd, pk);
sciMarkActiveStorySeen();
}
static void (*orig_overlaySetIsLiked)(id, SEL, BOOL, BOOL);
static void new_overlaySetIsLiked(id self, SEL _cmd, BOOL isLiked, BOOL animated) {
orig_overlaySetIsLiked(self, _cmd, isLiked, animated);
if (isLiked) sciMarkActiveStorySeen();
}
static void (*orig_handleLikeTap)(id, SEL, id);
static void new_handleLikeTap(id self, SEL _cmd, id button) {
orig_handleLikeTap(self, _cmd, button);
sciMarkActiveStorySeen();
}
static void (*orig_likeButtonSetIsLiked)(id, SEL, BOOL, BOOL);
static void new_likeButtonSetIsLiked(id self, SEL _cmd, BOOL isLiked, BOOL animated) {
orig_likeButtonSetIsLiked(self, _cmd, isLiked, animated);
if (isLiked) sciMarkActiveStorySeen();
}
%ctor {
Class overlayCtl = NSClassFromString(@"IGSundialViewerControlsOverlayController");
if (overlayCtl) {
SEL didLike = NSSelectorFromString(@"didLikeSundialWithMediaPK:");
if (class_getInstanceMethod(overlayCtl, didLike))
MSHookMessageEx(overlayCtl, didLike, (IMP)new_didLikeSundial, (IMP *)&orig_didLikeSundial);
SEL setLiked = @selector(setIsLiked:animated:);
if (class_getInstanceMethod(overlayCtl, setLiked))
MSHookMessageEx(overlayCtl, setLiked, (IMP)new_overlaySetIsLiked, (IMP *)&orig_overlaySetIsLiked);
}
Class likesImpl = NSClassFromString(@"IGStoryLikesInteractionControllingImpl");
if (likesImpl) {
SEL handleTap = NSSelectorFromString(@"handleStoryLikeTapWithButton:");
if (class_getInstanceMethod(likesImpl, handleTap))
MSHookMessageEx(likesImpl, handleTap, (IMP)new_handleLikeTap, (IMP *)&orig_handleLikeTap);
}
Class likeBtn = NSClassFromString(@"IGSundialViewerUFI.IGSundialLikeButton");
if (likeBtn) {
SEL setLiked = @selector(setIsLiked:animated:);
if (class_getInstanceMethod(likeBtn, setLiked))
MSHookMessageEx(likeBtn, setLiked, (IMP)new_likeButtonSetIsLiked, (IMP *)&orig_likeButtonSetIsLiked);
}
}
@@ -28,6 +28,13 @@ static NSMutableArray *sciPendingUpdates = nil;
// Server message ID -> timestamp the reason=2 (delete-for-you) was observed.
static NSMutableDictionary<NSString *, NSDate *> *sciDeleteForYouKeys = nil;
static NSMutableSet *sciPreservedIds = nil;
// Server message ID -> content class name for messages we recognize as
// reaction/action-log bookkeeping (e.g. "X liked a message" thread entries).
// Populated by hooking the data model class init. Used to skip preserving
// these IDs when their remove arrives.
static NSMutableDictionary<NSString *, NSString *> *sciMessageContentClasses = nil;
#define SCI_CONTENT_CLASSES_MAX 4000
#define SCI_PENDING_MAX 50
#define SCI_PRESERVED_IDS_KEY @"SCIPreservedMsgIds"
#define SCI_PRESERVED_MAX 200
@@ -53,6 +60,34 @@ void sciClearPreservedIds() {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:SCI_PRESERVED_IDS_KEY];
}
static NSMutableDictionary<NSString *, NSString *> *sciGetContentClasses() {
if (!sciMessageContentClasses) sciMessageContentClasses = [NSMutableDictionary dictionary];
return sciMessageContentClasses;
}
static void sciTrackInsertedMessage(NSString *sid, NSString *className) {
if (!sid.length || !className.length) return;
NSMutableDictionary *map = sciGetContentClasses();
map[sid] = className;
if (map.count > SCI_CONTENT_CLASSES_MAX) {
// Drop ~10% oldest by simply removing arbitrary keys
NSArray *keys = [map allKeys];
for (NSUInteger i = 0; i < keys.count / 10; i++) [map removeObjectForKey:keys[i]];
}
}
// Returns YES if the message at this server ID is known to be reaction-related
// (action log entry, reaction record, etc.) — i.e. should never be preserved.
static BOOL sciIsReactionRelatedMessage(NSString *sid) {
if (!sid.length) return NO;
NSString *className = sciGetContentClasses()[sid];
if (!className.length) return NO;
return [className containsString:@"Reaction"] ||
[className containsString:@"ActionLog"] ||
[className containsString:@"reaction"] ||
[className containsString:@"actionLog"];
}
// ============ ALLOC TRACKING ============
static id (*orig_msgUpdate_alloc)(id self, SEL _cmd);
@@ -62,13 +97,14 @@ static id new_msgUpdate_alloc(id self, SEL _cmd) {
if (!sciPendingUpdates) sciPendingUpdates = [NSMutableArray array];
@synchronized(sciPendingUpdates) {
[sciPendingUpdates addObject:instance];
while (sciPendingUpdates.count > 10)
while (sciPendingUpdates.count > SCI_PENDING_MAX)
[sciPendingUpdates removeObjectAtIndex:0];
}
}
return instance;
}
// ============ REMOTE UNSEND DETECTION ============
static NSString *sciExtractServerId(id key) {
@@ -92,13 +128,17 @@ static void sciPruneStaleDeleteForYouKeys() {
}
}
static BOOL sciConsumeRemoteUnsend() {
if (!sciPendingUpdates) return NO;
// Walks every pending IGDirectMessageUpdate, preserves the IDs of any reason=0
// remove that isn't a delete-for-you follow-up, and returns the set of preserved
// IDs. The caller decides whether to actually block + show a toast based on
// whether those IDs match real (rendered) messages.
static NSSet<NSString *> *sciConsumePendingPreserves() {
NSMutableSet<NSString *> *preserved = [NSMutableSet set];
if (!sciPendingUpdates) return preserved;
if (!sciDeleteForYouKeys) sciDeleteForYouKeys = [NSMutableDictionary dictionary];
sciPruneStaleDeleteForYouKeys();
BOOL shouldBlock = NO;
@synchronized(sciPendingUpdates) {
for (id update in [sciPendingUpdates copy]) {
@try {
@@ -114,8 +154,7 @@ static BOOL sciConsumeRemoteUnsend() {
reason = *(long long *)((char *)(__bridge void *)update + off);
}
// Delete-for-you initiator: remember the keys for the upcoming
// reason=0 follow-up so we don't block it.
// Delete-for-you initiator remember keys for the follow-up.
if (reason == 2) {
NSDate *now = [NSDate date];
for (id key in keys) {
@@ -125,44 +164,53 @@ static BOOL sciConsumeRemoteUnsend() {
continue;
}
if (reason == 0 && !sciLocalDeleteInProgress) {
// If every key matches a recent delete-for-you, this is the
// expected follow-up — let it through.
BOOL allMatched = YES;
for (id key in keys) {
NSString *sid = sciExtractServerId(key);
if (!sid || !sciDeleteForYouKeys[sid]) { allMatched = NO; break; }
}
if (allMatched) {
for (id key in keys) {
NSString *sid = sciExtractServerId(key);
if (sid) [sciDeleteForYouKeys removeObjectForKey:sid];
}
continue;
}
if (reason != 0 || sciLocalDeleteInProgress) continue;
// Otherwise this is a genuine remote unsend — preserve the
// affected message IDs and block the entire apply call.
// If every key matches a recent delete-for-you, drop the
// tracking entries and let it through (it's the follow-up).
BOOL allMatched = YES;
for (id key in keys) {
NSString *sid = sciExtractServerId(key);
if (!sid || !sciDeleteForYouKeys[sid]) { allMatched = NO; break; }
}
if (allMatched) {
for (id key in keys) {
NSString *sid = sciExtractServerId(key);
if (sid) [sciGetPreservedIds() addObject:sid];
if (sid) [sciDeleteForYouKeys removeObjectForKey:sid];
}
sciSavePreservedIds();
shouldBlock = YES;
break;
continue;
}
// Real remove — preserve only keys whose content class isn't a
// known reaction / action-log entry. Reaction events also fire
// reason=0 removes for the action-log record they create.
for (id key in keys) {
NSString *sid = sciExtractServerId(key);
if (!sid) continue;
if (sciIsReactionRelatedMessage(sid)) continue;
[sciGetPreservedIds() addObject:sid];
[preserved addObject:sid];
}
} @catch(id e) {}
}
[sciPendingUpdates removeAllObjects];
}
return shouldBlock;
if (preserved.count > 0) sciSavePreservedIds();
return preserved;
}
// ============ CACHE UPDATE HOOK ============
static void (*orig_applyUpdates)(id self, SEL _cmd, id updates, id completion, id userAccess);
static void new_applyUpdates(id self, SEL _cmd, id updates, id completion, id userAccess) {
if (sciKeepDeletedEnabled() && sciConsumeRemoteUnsend()) {
if (!sciKeepDeletedEnabled()) {
orig_applyUpdates(self, _cmd, updates, completion, userAccess);
return;
}
NSSet<NSString *> *preserved = sciConsumePendingPreserves();
if (preserved.count > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
// Refresh visible cells so newly preserved messages show the
// "Unsent" indicator immediately without waiting for a scroll.
@@ -371,15 +419,52 @@ static void new_cellLayoutSubviews(id self, SEL _cmd) {
sciUpdateCellIndicator(self);
}
// ============ ACTION LOG TRACKING ============
//
// IGDirectThreadActionLog is the local data-model class for "X liked a
// message" thread entries. IG instantiates one whenever an action log row
// is created — reaction add/remove, theme change, etc. We hook its full
// init, grab the message ID via the messageId getter, and store the class
// name in our content-class map. Later when a remove for that ID arrives,
// the consume path recognizes it as bookkeeping and skips preserving it.
static id (*orig_actionLogFullInit)(id, SEL, id, id, id, id, id, BOOL, BOOL, id);
static id new_actionLogFullInit(id self, SEL _cmd,
id message, id title, id textAttributes, id textParts,
id actionLogType, BOOL collapsible, BOOL hidden, id genAIMetadata) {
id result = orig_actionLogFullInit(self, _cmd, message, title, textAttributes, textParts,
actionLogType, collapsible, hidden, genAIMetadata);
@try {
SEL midSel = @selector(messageId);
if ([result respondsToSelector:midSel]) {
id mid = ((id(*)(id, SEL))objc_msgSend)(result, midSel);
if ([mid isKindOfClass:[NSString class]]) {
sciTrackInsertedMessage(mid, @"IGDirectThreadActionLog");
}
}
} @catch(id e) {}
return result;
}
// ============ RUNTIME HOOKS ============
%ctor {
// Action log entries (e.g. "X liked a message") — record their message IDs
// when IG creates them so we can later recognize a remove for those IDs as
// action-log bookkeeping rather than a real unsend.
Class actionLogCls = NSClassFromString(@"IGDirectThreadActionLog");
if (actionLogCls) {
SEL fullInit = NSSelectorFromString(@"initWithMessage:title:textAttributes:textParts:actionLogType:collapsible:hidden:genAIMetadata:");
if (class_getInstanceMethod(actionLogCls, fullInit))
MSHookMessageEx(actionLogCls, fullInit, (IMP)new_actionLogFullInit, (IMP *)&orig_actionLogFullInit);
}
Class msgUpdateClass = NSClassFromString(@"IGDirectMessageUpdate");
if (msgUpdateClass) {
MSHookMessageEx(object_getClass(msgUpdateClass), @selector(alloc),
(IMP)new_msgUpdate_alloc, (IMP *)&orig_msgUpdate_alloc);
}
Class cacheClass = NSClassFromString(@"IGDirectCacheUpdatesApplicator");
if (cacheClass) {
SEL sel = NSSelectorFromString(@"_applyThreadUpdates:completion:userAccess:");
@@ -155,6 +155,7 @@ static void sciDownloadDMVisualMessage(UIViewController *dmVC) {
}
}
// download handler — works for both stories and DM visual messages
%new - (void)sciDownloadTapped:(UIButton *)sender {
UIImpactFeedbackGenerator *haptic = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
@@ -280,4 +281,32 @@ static void sciDownloadDMVisualMessage(UIViewController *dmVC) {
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:@"Error: %@", e.reason]];
}
}
%end
// Mirror IG's chrome alpha onto our injected seen + download buttons so they
// fade in sync during hold/zoom. Walks up from a fading sibling to find the
// IGStoryFullscreenOverlayView and updates its tagged subviews.
static void sciSyncStoryButtonsAlpha(UIView *self_, CGFloat alpha) {
Class overlayCls = NSClassFromString(@"IGStoryFullscreenOverlayView");
if (!overlayCls) return;
UIView *cur = self_;
while (cur) {
for (UIView *sib in cur.superview.subviews) {
if (![sib isKindOfClass:overlayCls]) continue;
UIView *seen = [sib viewWithTag:1339];
UIView *dl = [sib viewWithTag:1340];
if (seen) seen.alpha = alpha;
if (dl) dl.alpha = alpha;
return;
}
cur = cur.superview;
}
}
%hook IGStoryFullscreenHeaderView
- (void)setAlpha:(CGFloat)alpha {
%orig;
sciSyncStoryButtonsAlpha((UIView *)self, alpha);
}
%end