From 4b9e0ec15b49b861b9bccda8c7150fb1c5e04684 Mon Sep 17 00:00:00 2001 From: faroukbmiled Date: Wed, 1 Apr 2026 10:12:42 +0100 Subject: [PATCH] Added download btn to stories ,fixed finger gestures for stories ,fixed manual view ,added not setting visually seen, added stop auto advance stories and more --- control | 2 +- src/Downloader/Download.m | 6 +- .../StoriesAndMessages/DisableStorySeen.x | 523 +++++++++++------- src/Settings/TweakSettings.m | 5 +- src/Tweak.x | 5 +- 5 files changed, 342 insertions(+), 199 deletions(-) diff --git a/control b/control index 46ef400..d620904 100644 --- a/control +++ b/control @@ -1,6 +1,6 @@ Package: com.socuul.scinsta Name: SCInsta -Version: 1.1.2 +Version: 1.1.3 Architecture: iphoneos-arm Description: A feature-rich tweak for Instagram on iOS! Homepage: https://github.com/SoCuul/SCInsta diff --git a/src/Downloader/Download.m b/src/Downloader/Download.m index 7ab86fd..91e555b 100644 --- a/src/Downloader/Download.m +++ b/src/Downloader/Download.m @@ -145,9 +145,9 @@ [weakSelf.downloadManager cancelDownload]; }; - UIViewController *topVC = topMostController(); - UIView *hostView = topVC.view; - if (!hostView) hostView = [UIApplication sharedApplication].keyWindow; + // Show on keyWindow so it survives VC transitions (e.g. leaving stories) + UIView *hostView = [UIApplication sharedApplication].keyWindow; + if (!hostView) hostView = topMostController().view; if (!hostView) { NSLog(@"[SCInsta] Download: No valid view"); return; diff --git a/src/Features/StoriesAndMessages/DisableStorySeen.x b/src/Features/StoriesAndMessages/DisableStorySeen.x index 765cab0..9cd2bb9 100644 --- a/src/Features/StoriesAndMessages/DisableStorySeen.x +++ b/src/Features/StoriesAndMessages/DisableStorySeen.x @@ -1,245 +1,384 @@ #import "../../Utils.h" #import "../../InstagramHeaders.h" +#import "../../Downloader/Download.h" +#import +#import -// Bypass flag: when YES, all hooks let calls through (for manual mark as seen) +// === State === static BOOL sciSeenBypassActive = NO; +static NSMutableSet *sciAllowedSeenPKs = nil; -static BOOL sciShouldBlockSeen() { +// === Helpers === +typedef id (*SCIMsgSend)(id, SEL); +typedef id (*SCIMsgSend1)(id, SEL, id); + +static id sciCall(id obj, SEL sel) { + if (!obj || ![obj respondsToSelector:sel]) return nil; + return ((SCIMsgSend)objc_msgSend)(obj, sel); +} +static id sciCall1(id obj, SEL sel, id arg1) { + if (!obj || ![obj respondsToSelector:sel]) return nil; + return ((SCIMsgSend1)objc_msgSend)(obj, sel, arg1); +} + +static void sciAllowSeenForPK(id media) { + if (!media) return; + id pk = sciCall(media, @selector(pk)); + if (!pk) return; + if (!sciAllowedSeenPKs) sciAllowedSeenPKs = [NSMutableSet set]; + NSString *pkStr = [NSString stringWithFormat:@"%@", pk]; + [sciAllowedSeenPKs addObject:pkStr]; + NSLog(@"[SCInsta] Allow-listed PK: %@", pkStr); +} + +static BOOL sciIsPKAllowed(id media) { + if (!media || !sciAllowedSeenPKs || sciAllowedSeenPKs.count == 0) return NO; + id pk = sciCall(media, @selector(pk)); + if (!pk) return NO; + return [sciAllowedSeenPKs containsObject:[NSString stringWithFormat:@"%@", pk]]; +} + +static BOOL sciShouldBlockSeenNetwork() { if (sciSeenBypassActive) return NO; return [SCIUtils getBoolPref:@"no_seen_receipt"]; } -// Block story seen receipts by intercepting all known upload/send paths -%hook IGStorySeenStateUploader -- (id)initWithUserSessionPK:(id)arg1 networker:(id)arg2 { - if (sciShouldBlockSeen()) { - NSLog(@"[SCInsta] Blocked story seen uploader init"); - return nil; - } - return %orig; +static BOOL sciShouldBlockSeenVisual() { + if (sciSeenBypassActive) return NO; + return [SCIUtils getBoolPref:@"no_seen_receipt"] && [SCIUtils getBoolPref:@"no_seen_visual"]; } + +static UIViewController * _Nullable sciFindVC(UIResponder *start, NSString *className) { + Class cls = NSClassFromString(className); + if (!cls) return nil; + UIResponder *r = start; + while (r) { + if ([r isKindOfClass:cls]) return (UIViewController *)r; + r = [r nextResponder]; + } + return nil; +} + +static IGMedia * _Nullable sciExtractMediaFromItem(id item) { + if (!item) return nil; + Class mediaClass = NSClassFromString(@"IGMedia"); + if (!mediaClass) return nil; + NSArray *trySelectors = @[@"media", @"mediaItem", @"storyItem", @"item", + @"feedItem", @"igMedia", @"model", @"backingModel", + @"storyMedia", @"mediaModel"]; + for (NSString *selName in trySelectors) { + id val = sciCall(item, NSSelectorFromString(selName)); + if (val && [val isKindOfClass:mediaClass]) return (IGMedia *)val; + } + unsigned int iCount = 0; + Ivar *ivars = class_copyIvarList([item class], &iCount); + for (unsigned int i = 0; i < iCount; i++) { + const char *type = ivar_getTypeEncoding(ivars[i]); + if (type && type[0] == '@') { + id val = object_getIvar(item, ivars[i]); + if (val && [val isKindOfClass:mediaClass]) { free(ivars); return (IGMedia *)val; } + } + } + if (ivars) free(ivars); + return nil; +} + +static id _Nullable sciGetCurrentStoryItem(UIResponder *start) { + UIViewController *storyVC = sciFindVC(start, @"IGStoryViewerViewController"); + if (!storyVC) return nil; + id vm = sciCall(storyVC, @selector(currentViewModel)); + if (!vm) return nil; + return sciCall1(storyVC, @selector(currentStoryItemForViewModel:), vm); +} + +// Find section controller: VC -> collectionView -> visibleCell -> containerView -> delegate +static id _Nullable sciFindSectionController(UIViewController *storyVC) { + Class sectionClass = NSClassFromString(@"IGStoryFullscreenSectionController"); + if (!sectionClass || !storyVC) return nil; + + // Find collection view in VC ivars + unsigned int count = 0; + Ivar *ivars = class_copyIvarList([storyVC class], &count); + UICollectionView *cv = nil; + for (unsigned int i = 0; i < count; i++) { + const char *type = ivar_getTypeEncoding(ivars[i]); + if (!type || type[0] != '@') continue; + id val = object_getIvar(storyVC, ivars[i]); + if (val && [val isKindOfClass:[UICollectionView class]]) { cv = val; break; } + } + if (ivars) free(ivars); + if (!cv) return nil; + + // Scan visible cells -> containerView -> delegate + for (UICollectionViewCell *cell in cv.visibleCells) { + unsigned int cCount = 0; + Ivar *cIvars = class_copyIvarList([cell class], &cCount); + for (unsigned int i = 0; i < cCount; i++) { + const char *type = ivar_getTypeEncoding(cIvars[i]); + if (!type || type[0] != '@') continue; + id val = object_getIvar(cell, cIvars[i]); + if (!val) continue; + // Check val's ivars for section controller (L4: cell.containerView.delegate) + unsigned int vCount = 0; + Ivar *vIvars = class_copyIvarList([val class], &vCount); + for (unsigned int j = 0; j < vCount; j++) { + const char *type2 = ivar_getTypeEncoding(vIvars[j]); + if (!type2 || type2[0] != '@') continue; + id val2 = object_getIvar(val, vIvars[j]); + if (val2 && [val2 isKindOfClass:sectionClass]) { free(vIvars); free(cIvars); return val2; } + } + if (vIvars) free(vIvars); + } + if (cIvars) free(cIvars); + } + return nil; +} + +// Story downloaders +static SCIDownloadDelegate *sciStoryVideoDl = nil; +static SCIDownloadDelegate *sciStoryImageDl = nil; + +static void sciInitStoryDownloaders() { + NSString *method = [SCIUtils getStringPref:@"dw_save_action"]; + DownloadAction action = [method isEqualToString:@"photos"] ? saveToPhotos : share; + DownloadAction imgAction = [method isEqualToString:@"photos"] ? saveToPhotos : quickLook; + sciStoryVideoDl = [[SCIDownloadDelegate alloc] initWithAction:action showProgress:YES]; + sciStoryImageDl = [[SCIDownloadDelegate alloc] initWithAction:imgAction showProgress:NO]; +} + +static void sciDownloadMedia(IGMedia *media) { + sciInitStoryDownloaders(); + NSURL *videoUrl = [SCIUtils getVideoUrlForMedia:media]; + if (videoUrl) { + [sciStoryVideoDl downloadFileWithURL:videoUrl fileExtension:[[videoUrl lastPathComponent] pathExtension] hudLabel:nil]; + return; + } + NSURL *photoUrl = [SCIUtils getPhotoUrlForMedia:media]; + if (photoUrl) { + [sciStoryImageDl downloadFileWithURL:photoUrl fileExtension:[[photoUrl lastPathComponent] pathExtension] hudLabel:nil]; + return; + } + [SCIUtils showErrorHUDWithDescription:@"Could not extract URL from story"]; +} + +// ============ BLOCK NETWORK SEEN ============ + +%hook IGStorySeenStateUploader - (void)uploadSeenStateWithMedia:(id)arg1 { - if (sciShouldBlockSeen()) return; + // Allow if: bypass active, or this specific media was manually marked + if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return; %orig; } - (void)uploadSeenState { - if (sciShouldBlockSeen()) return; + // Batch upload — allow if bypass or any manual PKs are pending + if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !(sciAllowedSeenPKs && sciAllowedSeenPKs.count > 0)) return; %orig; } - (void)_uploadSeenState:(id)arg1 { - if (sciShouldBlockSeen()) return; + if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return; %orig; } - (void)sendSeenReceipt:(id)arg1 { - if (sciShouldBlockSeen()) return; + if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return; %orig; } -- (id)networker { - if (sciShouldBlockSeen()) return nil; - return %orig; -} +// NEVER block networker — returning nil breaks the uploader permanently +- (id)networker { return %orig; } %end -// Block seen tracking on fullscreen section controller +// ============ BLOCK VISUAL SEEN ============ + %hook IGStoryFullscreenSectionController -- (void)markItemAsSeen:(id)arg1 { - if (sciShouldBlockSeen()) return; +// Visual seen blocking +- (void)markItemAsSeen:(id)arg1 { if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg1)) return; %orig; } +- (void)_markItemAsSeen:(id)arg1 { if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg1)) return; %orig; } +- (void)storySeenStateDidChange:(id)arg1 { if (sciShouldBlockSeenVisual()) return; %orig; } +- (void)sendSeenRequestForCurrentItem { if (sciShouldBlockSeenVisual()) return; %orig; } +- (void)markCurrentItemAsSeen { if (sciShouldBlockSeenVisual()) return; %orig; } + +// Stop auto-advance: block timer-triggered advances, allow manual taps +- (void)storyPlayerMediaViewDidPlayToEnd:(id)arg1 { + if ([SCIUtils getBoolPref:@"stop_story_auto_advance"]) return; %orig; } -- (void)_markItemAsSeen:(id)arg1 { - if (sciShouldBlockSeen()) return; - %orig; -} -- (void)storySeenStateDidChange:(id)arg1 { - if (sciShouldBlockSeen()) return; - %orig; -} -- (void)sendSeenRequestForCurrentItem { - if (sciShouldBlockSeen()) return; - %orig; -} -- (void)markCurrentItemAsSeen { - if (sciShouldBlockSeen()) return; +- (void)advanceToNextReelForAutoScroll { + if ([SCIUtils getBoolPref:@"stop_story_auto_advance"]) return; %orig; } %end -// Block seen on viewer controller %hook IGStoryViewerViewController -- (void)markAsSeen { - if (sciShouldBlockSeen()) return; - %orig; -} -- (void)markStoryAsSeen:(id)arg1 { - if (sciShouldBlockSeen()) return; - %orig; -} -- (void)_markCurrentStoryAsSeen { - if (sciShouldBlockSeen()) return; - %orig; -} -- (void)markCurrentMediaAsSeen { - if (sciShouldBlockSeen()) return; +- (void)fullscreenSectionController:(id)arg1 didMarkItemAsSeen:(id)arg2 { + if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg2)) return; %orig; } %end -// Block local visual seen state updates on the story tray -// This prevents the colored ring from turning grey after viewing %hook IGStoryTrayViewModel -- (void)markAsSeen { - if (sciShouldBlockSeen()) return; - %orig; -} -- (void)setHasUnseenMedia:(BOOL)arg1 { - if (sciShouldBlockSeen()) { - // Always keep as unseen visually - %orig(YES); - return; - } - %orig; -} -- (BOOL)hasUnseenMedia { - if (sciShouldBlockSeen()) return YES; - return %orig; -} -- (void)setIsSeen:(BOOL)arg1 { - if (sciShouldBlockSeen()) { - %orig(NO); - return; - } - %orig; -} -- (BOOL)isSeen { - if (sciShouldBlockSeen()) return NO; - return %orig; -} +- (void)markAsSeen { if (sciShouldBlockSeenVisual()) return; %orig; } +- (void)setHasUnseenMedia:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(YES); return; } %orig; } +- (BOOL)hasUnseenMedia { if (sciShouldBlockSeenVisual()) return YES; return %orig; } +- (void)setIsSeen:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; } +- (BOOL)isSeen { if (sciShouldBlockSeenVisual()) return NO; return %orig; } %end -// Also try to block on the story item model level %hook IGStoryItem -- (void)setHasSeen:(BOOL)arg1 { - if (sciShouldBlockSeen()) { - %orig(NO); - return; - } - %orig; -} -- (BOOL)hasSeen { - if (sciShouldBlockSeen()) return NO; - return %orig; -} +- (void)setHasSeen:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; } +- (BOOL)hasSeen { if (sciShouldBlockSeenVisual()) return NO; return %orig; } %end -// Manual "mark as seen" button on story overlay +%hook IGStoryGradientRingView +- (void)setIsSeen:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; } +- (void)setSeen:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; } +- (void)updateRingForSeenState:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; } +%end + +// ============ OVERLAY BUTTONS ============ + %hook IGStoryFullscreenOverlayView - (void)didMoveToSuperview { %orig; + if (!self.superview) return; - if (!sciShouldBlockSeen()) return; - if ([self viewWithTag:1339]) return; - - UIButton *seenBtn = [UIButton buttonWithType:UIButtonTypeCustom]; - seenBtn.tag = 1339; - - UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:14 weight:UIImageSymbolWeightMedium]; - UIImage *icon = [UIImage systemImageNamed:@"eye" withConfiguration:config]; - [seenBtn setImage:icon forState:UIControlStateNormal]; - [seenBtn setTitle:@" Mark seen" forState:UIControlStateNormal]; - [seenBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; - seenBtn.titleLabel.font = [UIFont systemFontOfSize:11 weight:UIFontWeightMedium]; - seenBtn.tintColor = [UIColor whiteColor]; - seenBtn.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4]; - seenBtn.layer.cornerRadius = 14; - seenBtn.clipsToBounds = YES; - seenBtn.contentEdgeInsets = UIEdgeInsetsMake(6, 10, 6, 12); - - seenBtn.translatesAutoresizingMaskIntoConstraints = NO; - [seenBtn addTarget:self action:@selector(sciMarkSeenTapped:) forControlEvents:UIControlEventTouchUpInside]; - [self addSubview:seenBtn]; - - // Bottom right, moved up to avoid overlapping existing buttons - [NSLayoutConstraint activateConstraints:@[ - [seenBtn.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor constant:-110], - [seenBtn.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-12], - [seenBtn.heightAnchor constraintEqualToConstant:28] - ]]; -} - -%new - (void)sciMarkSeenTapped:(UIButton *)sender { - // Haptic feedback - UIImpactFeedbackGenerator *haptic = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; - [haptic impactOccurred]; - - // Visual feedback - [UIView animateWithDuration:0.1 animations:^{ - sender.transform = CGAffineTransformMakeScale(0.85, 0.85); - sender.alpha = 0.6; - } completion:^(BOOL finished) { - [UIView animateWithDuration:0.15 animations:^{ - sender.transform = CGAffineTransformIdentity; - sender.alpha = 1.0; - }]; - }]; - - // Enable bypass so all our hooks let the calls through - sciSeenBypassActive = YES; - - BOOL didMark = NO; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - // Try all view controllers in responder chain - UIResponder *responder = self; - while (responder) { - // IGStoryViewerViewController - if ([responder isKindOfClass:NSClassFromString(@"IGStoryViewerViewController")]) { - SEL selectors[] = { - @selector(markAsSeen), @selector(markStoryAsSeen:), - @selector(_markCurrentStoryAsSeen), @selector(markCurrentMediaAsSeen) - }; - for (int i = 0; i < 4; i++) { - if ([responder respondsToSelector:selectors[i]]) { - NSLog(@"[SCInsta] Manual seen: calling %@ on IGStoryViewerViewController", NSStringFromSelector(selectors[i])); - if (selectors[i] == @selector(markStoryAsSeen:)) { - [responder performSelector:selectors[i] withObject:nil]; - } else { - [responder performSelector:selectors[i]]; - } - didMark = YES; - } - } - } - // IGStoryFullscreenSectionController (might be in responder chain as next responder of a child VC) - if ([responder isKindOfClass:NSClassFromString(@"IGStoryFullscreenSectionController")]) { - SEL selectors[] = { - @selector(markItemAsSeen:), @selector(markCurrentItemAsSeen), - @selector(sendSeenRequestForCurrentItem) - }; - for (int i = 0; i < 3; i++) { - if ([responder respondsToSelector:selectors[i]]) { - NSLog(@"[SCInsta] Manual seen: calling %@ on IGStoryFullscreenSectionController", NSStringFromSelector(selectors[i])); - if (selectors[i] == @selector(markItemAsSeen:)) { - [responder performSelector:selectors[i] withObject:nil]; - } else { - [responder performSelector:selectors[i]]; - } - didMark = YES; - } - } - } - responder = [responder nextResponder]; + if ([SCIUtils getBoolPref:@"dw_story"] && ![self viewWithTag:1340]) { + UIButton *dlBtn = [UIButton buttonWithType:UIButtonTypeCustom]; + dlBtn.tag = 1340; + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:18 weight:UIImageSymbolWeightSemibold]; + [dlBtn setImage:[UIImage systemImageNamed:@"arrow.down" withConfiguration:config] forState:UIControlStateNormal]; + dlBtn.tintColor = [UIColor whiteColor]; + dlBtn.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4]; + dlBtn.layer.cornerRadius = 18; + dlBtn.clipsToBounds = YES; + dlBtn.translatesAutoresizingMaskIntoConstraints = NO; + [dlBtn addTarget:self action:@selector(sciStoryDownloadTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:dlBtn]; + [NSLayoutConstraint activateConstraints:@[ + [dlBtn.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor constant:-100], + [dlBtn.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-12], + [dlBtn.widthAnchor constraintEqualToConstant:36], + [dlBtn.heightAnchor constraintEqualToConstant:36] + ]]; } -#pragma clang diagnostic pop + if ([SCIUtils getBoolPref:@"no_seen_receipt"] && ![self viewWithTag:1339]) { + UIButton *seenBtn = [UIButton buttonWithType:UIButtonTypeCustom]; + seenBtn.tag = 1339; + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:18 weight:UIImageSymbolWeightSemibold]; + [seenBtn setImage:[UIImage systemImageNamed:@"eye" withConfiguration:config] forState:UIControlStateNormal]; + seenBtn.tintColor = [UIColor whiteColor]; + seenBtn.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4]; + seenBtn.layer.cornerRadius = 18; + seenBtn.clipsToBounds = YES; + seenBtn.translatesAutoresizingMaskIntoConstraints = NO; + [seenBtn addTarget:self action:@selector(sciMarkSeenTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:seenBtn]; + UIView *dlBtn = [self viewWithTag:1340]; + if (dlBtn) { + [NSLayoutConstraint activateConstraints:@[ + [seenBtn.centerYAnchor constraintEqualToAnchor:dlBtn.centerYAnchor], + [seenBtn.trailingAnchor constraintEqualToAnchor:dlBtn.leadingAnchor constant:-10], + [seenBtn.widthAnchor constraintEqualToConstant:36], + [seenBtn.heightAnchor constraintEqualToConstant:36] + ]]; + } else { + [NSLayoutConstraint activateConstraints:@[ + [seenBtn.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor constant:-100], + [seenBtn.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-12], + [seenBtn.widthAnchor constraintEqualToConstant:36], + [seenBtn.heightAnchor constraintEqualToConstant:36] + ]]; + } + } +} - // Re-enable blocking - sciSeenBypassActive = NO; +// ============ STORY DOWNLOAD ============ - if (didMark) { - [SCIUtils showToastForDuration:1.5 title:@"Marked as seen"]; - } else { - [SCIUtils showToastForDuration:2.0 title:@"Could not mark as seen" subtitle:@"Method not found"]; +%new - (void)sciStoryDownloadTapped:(UIButton *)sender { + UIImpactFeedbackGenerator *haptic = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; + [haptic impactOccurred]; + [UIView animateWithDuration:0.1 animations:^{ sender.transform = CGAffineTransformMakeScale(0.8, 0.8); } + completion:^(BOOL f) { [UIView animateWithDuration:0.1 animations:^{ sender.transform = CGAffineTransformIdentity; }]; }]; + @try { + id item = sciGetCurrentStoryItem(self); + IGMedia *media = sciExtractMediaFromItem(item); + if (media) { + if ([SCIUtils getBoolPref:@"dw_confirm"]) { + [SCIUtils showConfirmation:^{ sciDownloadMedia(media); } title:@"Download story?"]; + } else { + sciDownloadMedia(media); + } + return; + } + [SCIUtils showErrorHUDWithDescription:@"Could not find story media"]; + } @catch (NSException *e) { + [SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:@"Error: %@", e.reason]]; + } +} + +// ============ MARK SEEN ============ + +%new - (void)sciMarkSeenTapped:(UIButton *)sender { + UIImpactFeedbackGenerator *haptic = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; + [haptic impactOccurred]; + [UIView animateWithDuration:0.1 animations:^{ sender.transform = CGAffineTransformMakeScale(0.8, 0.8); sender.alpha = 0.6; } + completion:^(BOOL f) { [UIView animateWithDuration:0.15 animations:^{ sender.transform = CGAffineTransformIdentity; sender.alpha = 1.0; }]; }]; + + @try { + UIViewController *storyVC = sciFindVC(self, @"IGStoryViewerViewController"); + if (!storyVC) { [SCIUtils showErrorHUDWithDescription:@"Story VC not found"]; return; } + + // Get current story media + id sectionCtrl = sciFindSectionController(storyVC); + id storyItem = sectionCtrl ? sciCall(sectionCtrl, NSSelectorFromString(@"currentStoryItem")) : nil; + if (!storyItem) storyItem = sciGetCurrentStoryItem(self); + IGMedia *media = (storyItem && [storyItem isKindOfClass:NSClassFromString(@"IGMedia")]) ? storyItem : sciExtractMediaFromItem(storyItem); + + if (!media) { [SCIUtils showErrorHUDWithDescription:@"Could not find story media"]; return; } + + // Add this media PK to the permanent allow list + // When Instagram's deferred upload eventually fires, our hooks will let this PK through + sciAllowSeenForPK(media); + + // Also set bypass for immediate calls + sciSeenBypassActive = YES; + + // Trigger the visual seen update via VC delegate + SEL delegateSel = @selector(fullscreenSectionController:didMarkItemAsSeen:); + if ([storyVC respondsToSelector:delegateSel]) { + typedef void (*Func)(id, SEL, id, id); + ((Func)objc_msgSend)(storyVC, delegateSel, sectionCtrl, media); + } + + // Trigger the section controller's mark flow + if (sectionCtrl) { + SEL markSel = NSSelectorFromString(@"markItemAsSeen:"); + if ([sectionCtrl respondsToSelector:markSel]) { + ((SCIMsgSend1)objc_msgSend)(sectionCtrl, markSel, media); + } + } + + // Update the session seen state manager + id seenManager = sciCall(storyVC, @selector(viewingSessionSeenStateManager)); + id vm = sciCall(storyVC, @selector(currentViewModel)); + if (seenManager && vm) { + SEL setSeenSel = NSSelectorFromString(@"setSeenMediaId:forReelPK:"); + if ([seenManager respondsToSelector:setSeenSel]) { + id mediaPK = sciCall(media, @selector(pk)); + id reelPK = sciCall(vm, NSSelectorFromString(@"reelPK")); + if (!reelPK) reelPK = sciCall(vm, @selector(pk)); + if (mediaPK && reelPK) { + typedef void (*SetFunc)(id, SEL, id, id); + ((SetFunc)objc_msgSend)(seenManager, setSeenSel, mediaPK, reelPK); + } + } + } + + sciSeenBypassActive = NO; + + [SCIUtils showToastForDuration:2.0 title:@"Marked as seen" subtitle:@"Will sync when leaving stories"]; + } @catch (NSException *e) { + sciSeenBypassActive = NO; + [SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:@"Error: %@", e.reason]]; } } %end diff --git a/src/Settings/TweakSettings.m b/src/Settings/TweakSettings.m index 49a064a..17cb1de 100644 --- a/src/Settings/TweakSettings.m +++ b/src/Settings/TweakSettings.m @@ -150,6 +150,8 @@ [SCISetting switchCellWithTitle:@"Disable view-once limitations" subtitle:@"Makes view-once messages behave like normal visual messages (loopable/pauseable)" defaultsKey:@"disable_view_once_limitations"], [SCISetting switchCellWithTitle:@"Disable screenshot detection" subtitle:@"Removes the screenshot-prevention features for visual messages in DMs" defaultsKey:@"remove_screenshot_alert"], [SCISetting switchCellWithTitle:@"Disable story seen receipt" subtitle:@"Hides the notification for others when you view their story" defaultsKey:@"no_seen_receipt"], + [SCISetting switchCellWithTitle:@"Keep stories visually unseen" subtitle:@"Prevents stories from visually marking as seen in the tray (keeps colorful ring)" defaultsKey:@"no_seen_visual"], + [SCISetting switchCellWithTitle:@"Stop story auto-advance" subtitle:@"Stories won't auto-skip to the next one when the timer ends. Tap to advance manually" defaultsKey:@"stop_story_auto_advance"], [SCISetting switchCellWithTitle:@"Disable instants creation" subtitle:@"Hides the functionality to create/send instants" defaultsKey:@"disable_instants_creation" requiresRestart:YES] ] }] @@ -282,9 +284,10 @@ @"header": @"Credits", @"rows": @[ [SCISetting linkCellWithTitle:@"Developer" subtitle:@"SoCuul" imageUrl:@"https://i.imgur.com/c9CbytZ.png" url:@"https://socuul.dev"], + [SCISetting linkCellWithTitle:@"Modded by" subtitle:@"Ryuk" imageUrl:@"https://github.com/faroukbmiled.png" url:@"https://github.com/faroukbmiled"], [SCISetting linkCellWithTitle:@"View Repo" subtitle:@"View the tweak's source code on GitHub" imageUrl:@"https://i.imgur.com/BBUNzeP.png" url:@"https://github.com/SoCuul/SCInsta"] ], - @"footer": [NSString stringWithFormat:@"SCInsta %@\n\nInstagram v%@", SCIVersionString, [SCIUtils IGVersionString]] + @"footer": [NSString stringWithFormat:@"SCInsta %@\n\nInstagram v%@\n\nModded by Ryuk", SCIVersionString, [SCIUtils IGVersionString]] } ]; } diff --git a/src/Tweak.x b/src/Tweak.x index 93c93f9..54a4446 100644 --- a/src/Tweak.x +++ b/src/Tweak.x @@ -13,7 +13,7 @@ /////////////////////////////////////////////////////////// // * Tweak version * -NSString *SCIVersionString = @"v1.1.2"; +NSString *SCIVersionString = @"v1.1.3"; // Variables that work across features BOOL dmVisualMsgsViewedButtonEnabled = false; @@ -44,7 +44,8 @@ BOOL dmVisualMsgsViewedButtonEnabled = false; @"enable_notes_customization": @(YES), @"custom_note_themes": @(YES), @"disable_auto_unmuting_reels": @(YES), - @"doom_scrolling_reel_count": @(1) + @"doom_scrolling_reel_count": @(1), + @"no_seen_visual": @(YES) }; [[NSUserDefaults standardUserDefaults] registerDefaults:sciDefaults];