mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-09-23 09:50:57 +02:00
- Fixed liquid glass buttons and surfaces
- Fixed repost confirmation not working in reels - Fixed visual message seen bug (messages were never marked as viewed) - Download button works in DM disappearing messages (photos + videos) - Mark as viewed button for DM disappearing messages
This commit is contained in:
@@ -38,3 +38,5 @@ CLAUDE.md
|
|||||||
upstream-scinsta
|
upstream-scinsta
|
||||||
*.ipa
|
*.ipa
|
||||||
*.dylib
|
*.dylib
|
||||||
|
deploy.sh
|
||||||
|
PENDING_CHANGES.md
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ A feature-rich iOS tweak for Instagram, forked from [SCInsta](https://github.com
|
|||||||
- Manual mark story as seen — button on story overlay to selectively mark stories as seen **\***
|
- Manual mark story as seen — button on story overlay to selectively mark stories as seen **\***
|
||||||
- Stop story auto-advance — stories won't auto-skip when the timer ends **\***
|
- Stop story auto-advance — stories won't auto-skip when the timer ends **\***
|
||||||
- Story download button — download directly from the story overlay **\***
|
- Story download button — download directly from the story overlay **\***
|
||||||
|
- Download disappearing DM media (photos + videos) **\***
|
||||||
|
- Mark disappearing messages as viewed button **\***
|
||||||
- Disable instants creation
|
- Disable instants creation
|
||||||
|
|
||||||
### Navigation
|
### Navigation
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Package: com.faroukbmiled.ryukgram
|
Package: com.faroukbmiled.ryukgram
|
||||||
Name: RyukGram
|
Name: RyukGram
|
||||||
Version: 1.1.4
|
Version: 1.1.5
|
||||||
Architecture: iphoneos-arm
|
Architecture: iphoneos-arm
|
||||||
Description: A feature-rich tweak for Instagram on iOS, based on SCInsta
|
Description: A feature-rich tweak for Instagram on iOS, based on SCInsta
|
||||||
Homepage: https://github.com/faroukbmiled/RyukGram
|
Homepage: https://github.com/faroukbmiled/RyukGram
|
||||||
|
|||||||
@@ -1,34 +1,15 @@
|
|||||||
#import "../../Utils.h"
|
// Story seen receipt blocking + visual seen state blocking
|
||||||
#import "../../InstagramHeaders.h"
|
#import "StoryHelpers.h"
|
||||||
#import "../../Downloader/Download.h"
|
|
||||||
#import <objc/runtime.h>
|
|
||||||
#import <objc/message.h>
|
|
||||||
|
|
||||||
// === State ===
|
BOOL sciSeenBypassActive = NO;
|
||||||
static BOOL sciSeenBypassActive = NO;
|
NSMutableSet *sciAllowedSeenPKs = nil;
|
||||||
static NSMutableSet *sciAllowedSeenPKs = nil;
|
|
||||||
|
|
||||||
// === Helpers ===
|
void sciAllowSeenForPK(id media) {
|
||||||
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;
|
if (!media) return;
|
||||||
id pk = sciCall(media, @selector(pk));
|
id pk = sciCall(media, @selector(pk));
|
||||||
if (!pk) return;
|
if (!pk) return;
|
||||||
if (!sciAllowedSeenPKs) sciAllowedSeenPKs = [NSMutableSet set];
|
if (!sciAllowedSeenPKs) sciAllowedSeenPKs = [NSMutableSet set];
|
||||||
NSString *pkStr = [NSString stringWithFormat:@"%@", pk];
|
[sciAllowedSeenPKs addObject:[NSString stringWithFormat:@"%@", pk]];
|
||||||
[sciAllowedSeenPKs addObject:pkStr];
|
|
||||||
NSLog(@"[SCInsta] Allow-listed PK: %@", pkStr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static BOOL sciIsPKAllowed(id media) {
|
static BOOL sciIsPKAllowed(id media) {
|
||||||
@@ -48,129 +29,13 @@ static BOOL sciShouldBlockSeenVisual() {
|
|||||||
return [SCIUtils getBoolPref:@"no_seen_receipt"] && [SCIUtils getBoolPref:@"no_seen_visual"];
|
return [SCIUtils getBoolPref:@"no_seen_receipt"] && [SCIUtils getBoolPref:@"no_seen_visual"];
|
||||||
}
|
}
|
||||||
|
|
||||||
static UIViewController * _Nullable sciFindVC(UIResponder *start, NSString *className) {
|
// network seen blocking
|
||||||
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
|
%hook IGStorySeenStateUploader
|
||||||
- (void)uploadSeenStateWithMedia:(id)arg1 {
|
- (void)uploadSeenStateWithMedia:(id)arg1 {
|
||||||
// Allow if: bypass active, or this specific media was manually marked
|
|
||||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
|
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
|
||||||
%orig;
|
%orig;
|
||||||
}
|
}
|
||||||
- (void)uploadSeenState {
|
- (void)uploadSeenState {
|
||||||
// Batch upload — allow if bypass or any manual PKs are pending
|
|
||||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !(sciAllowedSeenPKs && sciAllowedSeenPKs.count > 0)) return;
|
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !(sciAllowedSeenPKs && sciAllowedSeenPKs.count > 0)) return;
|
||||||
%orig;
|
%orig;
|
||||||
}
|
}
|
||||||
@@ -182,21 +47,16 @@ static void sciDownloadMedia(IGMedia *media) {
|
|||||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
|
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
|
||||||
%orig;
|
%orig;
|
||||||
}
|
}
|
||||||
// NEVER block networker — returning nil breaks the uploader permanently
|
|
||||||
- (id)networker { return %orig; }
|
- (id)networker { return %orig; }
|
||||||
%end
|
%end
|
||||||
|
|
||||||
// ============ BLOCK VISUAL SEEN ============
|
// visual seen blocking + story auto-advance
|
||||||
|
|
||||||
%hook IGStoryFullscreenSectionController
|
%hook IGStoryFullscreenSectionController
|
||||||
// Visual seen blocking
|
|
||||||
- (void)markItemAsSeen:(id)arg1 { if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg1)) return; %orig; }
|
- (void)markItemAsSeen:(id)arg1 { if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg1)) return; %orig; }
|
||||||
- (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)storySeenStateDidChange:(id)arg1 { if (sciShouldBlockSeenVisual()) return; %orig; }
|
||||||
- (void)sendSeenRequestForCurrentItem { if (sciShouldBlockSeenVisual()) return; %orig; }
|
- (void)sendSeenRequestForCurrentItem { if (sciShouldBlockSeenVisual()) return; %orig; }
|
||||||
- (void)markCurrentItemAsSeen { if (sciShouldBlockSeenVisual()) return; %orig; }
|
- (void)markCurrentItemAsSeen { if (sciShouldBlockSeenVisual()) return; %orig; }
|
||||||
|
|
||||||
// Stop auto-advance: block timer-triggered advances, allow manual taps
|
|
||||||
- (void)storyPlayerMediaViewDidPlayToEnd:(id)arg1 {
|
- (void)storyPlayerMediaViewDidPlayToEnd:(id)arg1 {
|
||||||
if ([SCIUtils getBoolPref:@"stop_story_auto_advance"]) return;
|
if ([SCIUtils getBoolPref:@"stop_story_auto_advance"]) return;
|
||||||
%orig;
|
%orig;
|
||||||
@@ -232,153 +92,3 @@ static void sciDownloadMedia(IGMedia *media) {
|
|||||||
- (void)setSeen:(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; }
|
- (void)updateRingForSeenState:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; }
|
||||||
%end
|
%end
|
||||||
|
|
||||||
// ============ OVERLAY BUTTONS ============
|
|
||||||
|
|
||||||
%hook IGStoryFullscreenOverlayView
|
|
||||||
- (void)didMoveToSuperview {
|
|
||||||
%orig;
|
|
||||||
if (!self.superview) return;
|
|
||||||
|
|
||||||
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]
|
|
||||||
]];
|
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ STORY DOWNLOAD ============
|
|
||||||
|
|
||||||
%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
|
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
// Download + mark seen buttons on story/DM visual message overlay
|
||||||
|
#import "StoryHelpers.h"
|
||||||
|
|
||||||
|
extern "C" BOOL sciSeenBypassActive;
|
||||||
|
extern "C" NSMutableSet *sciAllowedSeenPKs;
|
||||||
|
extern "C" void sciAllowSeenForPK(id);
|
||||||
|
|
||||||
|
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"];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sciDownloadWithConfirm(void(^block)(void)) {
|
||||||
|
if ([SCIUtils getBoolPref:@"dw_confirm"]) {
|
||||||
|
[SCIUtils showConfirmation:block title:@"Download?"];
|
||||||
|
} else {
|
||||||
|
block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get media from DM visual message VC
|
||||||
|
static void sciDownloadDMVisualMessage(UIViewController *dmVC) {
|
||||||
|
Ivar dsIvar = class_getInstanceVariable([dmVC class], "_dataSource");
|
||||||
|
id ds = dsIvar ? object_getIvar(dmVC, dsIvar) : nil;
|
||||||
|
if (!ds) return;
|
||||||
|
Ivar msgIvar = class_getInstanceVariable([ds class], "_currentMessage");
|
||||||
|
id msg = msgIvar ? object_getIvar(ds, msgIvar) : nil;
|
||||||
|
if (!msg) return;
|
||||||
|
|
||||||
|
// video
|
||||||
|
id rawVideo = sciCall(msg, @selector(rawVideo));
|
||||||
|
if (rawVideo) {
|
||||||
|
NSURL *url = [SCIUtils getVideoUrl:rawVideo];
|
||||||
|
if (url) {
|
||||||
|
sciInitStoryDownloaders();
|
||||||
|
sciDownloadWithConfirm(^{ [sciStoryVideoDl downloadFileWithURL:url fileExtension:[[url lastPathComponent] pathExtension] hudLabel:nil]; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// photo via rawPhoto
|
||||||
|
id rawPhoto = sciCall(msg, @selector(rawPhoto));
|
||||||
|
if (rawPhoto) {
|
||||||
|
NSURL *url = [SCIUtils getPhotoUrl:rawPhoto];
|
||||||
|
if (url) {
|
||||||
|
sciInitStoryDownloaders();
|
||||||
|
sciDownloadWithConfirm(^{ [sciStoryImageDl downloadFileWithURL:url fileExtension:[[url lastPathComponent] pathExtension] hudLabel:nil]; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// photo via imageSpecifier
|
||||||
|
id imgSpec = sciCall(msg, NSSelectorFromString(@"imageSpecifier"));
|
||||||
|
if (imgSpec) {
|
||||||
|
NSURL *url = sciCall(imgSpec, @selector(url));
|
||||||
|
if (url) {
|
||||||
|
sciInitStoryDownloaders();
|
||||||
|
sciDownloadWithConfirm(^{ [sciStoryImageDl downloadFileWithURL:url fileExtension:[[url lastPathComponent] pathExtension] hudLabel:nil]; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// photo via _visualMediaInfo._media
|
||||||
|
Ivar vmiIvar = class_getInstanceVariable([msg class], "_visualMediaInfo");
|
||||||
|
id vmi = vmiIvar ? object_getIvar(msg, vmiIvar) : nil;
|
||||||
|
if (vmi) {
|
||||||
|
Ivar mediaIvar = class_getInstanceVariable([vmi class], "_media");
|
||||||
|
id mediaObj = mediaIvar ? object_getIvar(vmi, mediaIvar) : nil;
|
||||||
|
if (mediaObj) {
|
||||||
|
IGMedia *media = sciExtractMediaFromItem(mediaObj);
|
||||||
|
if (!media && [mediaObj isKindOfClass:NSClassFromString(@"IGMedia")]) media = (IGMedia *)mediaObj;
|
||||||
|
if (media) { sciDownloadWithConfirm(^{ sciDownloadMedia(media); }); return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SCIUtils showErrorHUDWithDescription:@"Could not find media"];
|
||||||
|
}
|
||||||
|
|
||||||
|
%hook IGStoryFullscreenOverlayView
|
||||||
|
- (void)didMoveToSuperview {
|
||||||
|
%orig;
|
||||||
|
if (!self.superview) return;
|
||||||
|
|
||||||
|
// download button
|
||||||
|
if ([SCIUtils getBoolPref:@"dw_story"] && ![self viewWithTag:1340]) {
|
||||||
|
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||||
|
btn.tag = 1340;
|
||||||
|
UIImageSymbolConfiguration *cfg = [UIImageSymbolConfiguration configurationWithPointSize:18 weight:UIImageSymbolWeightSemibold];
|
||||||
|
[btn setImage:[UIImage systemImageNamed:@"arrow.down" withConfiguration:cfg] forState:UIControlStateNormal];
|
||||||
|
btn.tintColor = [UIColor whiteColor];
|
||||||
|
btn.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4];
|
||||||
|
btn.layer.cornerRadius = 18;
|
||||||
|
btn.clipsToBounds = YES;
|
||||||
|
btn.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
[btn addTarget:self action:@selector(sciDownloadTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
[self addSubview:btn];
|
||||||
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
|
[btn.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor constant:-100],
|
||||||
|
[btn.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-12],
|
||||||
|
[btn.widthAnchor constraintEqualToConstant:36],
|
||||||
|
[btn.heightAnchor constraintEqualToConstant:36]
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark seen button (stories: mark as seen, DMs: mark as viewed + dismiss)
|
||||||
|
if ([SCIUtils getBoolPref:@"no_seen_receipt"] && ![self viewWithTag:1339]) {
|
||||||
|
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||||
|
btn.tag = 1339;
|
||||||
|
UIImageSymbolConfiguration *cfg = [UIImageSymbolConfiguration configurationWithPointSize:18 weight:UIImageSymbolWeightSemibold];
|
||||||
|
[btn setImage:[UIImage systemImageNamed:@"eye" withConfiguration:cfg] forState:UIControlStateNormal];
|
||||||
|
btn.tintColor = [UIColor whiteColor];
|
||||||
|
btn.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4];
|
||||||
|
btn.layer.cornerRadius = 18;
|
||||||
|
btn.clipsToBounds = YES;
|
||||||
|
btn.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
[btn addTarget:self action:@selector(sciMarkSeenTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
[self addSubview:btn];
|
||||||
|
UIView *dlBtn = [self viewWithTag:1340];
|
||||||
|
if (dlBtn) {
|
||||||
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
|
[btn.centerYAnchor constraintEqualToAnchor:dlBtn.centerYAnchor],
|
||||||
|
[btn.trailingAnchor constraintEqualToAnchor:dlBtn.leadingAnchor constant:-10],
|
||||||
|
[btn.widthAnchor constraintEqualToConstant:36],
|
||||||
|
[btn.heightAnchor constraintEqualToConstant:36]
|
||||||
|
]];
|
||||||
|
} else {
|
||||||
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
|
[btn.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor constant:-100],
|
||||||
|
[btn.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-12],
|
||||||
|
[btn.widthAnchor constraintEqualToConstant:36],
|
||||||
|
[btn.heightAnchor constraintEqualToConstant:36]
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// download handler — works for both stories and DM visual messages
|
||||||
|
%new - (void)sciDownloadTapped:(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 {
|
||||||
|
// story path
|
||||||
|
id item = sciGetCurrentStoryItem(self);
|
||||||
|
IGMedia *media = sciExtractMediaFromItem(item);
|
||||||
|
if (media) {
|
||||||
|
sciDownloadWithConfirm(^{ sciDownloadMedia(media); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DM visual message path
|
||||||
|
UIViewController *dmVC = sciFindVC(self, @"IGDirectVisualMessageViewerController");
|
||||||
|
if (dmVC) {
|
||||||
|
sciDownloadDMVisualMessage(dmVC);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SCIUtils showErrorHUDWithDescription:@"Could not find media"];
|
||||||
|
} @catch (NSException *e) {
|
||||||
|
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:@"Error: %@", e.reason]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark seen handler — stories: allow-list approach, DMs: trigger viewed + dismiss
|
||||||
|
%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 {
|
||||||
|
// story path
|
||||||
|
UIViewController *storyVC = sciFindVC(self, @"IGStoryViewerViewController");
|
||||||
|
if (storyVC) {
|
||||||
|
// allow-list the current media PK for deferred upload
|
||||||
|
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; }
|
||||||
|
|
||||||
|
sciAllowSeenForPK(media);
|
||||||
|
sciSeenBypassActive = YES;
|
||||||
|
|
||||||
|
SEL delegateSel = @selector(fullscreenSectionController:didMarkItemAsSeen:);
|
||||||
|
if ([storyVC respondsToSelector:delegateSel]) {
|
||||||
|
typedef void (*Func)(id, SEL, id, id);
|
||||||
|
((Func)objc_msgSend)(storyVC, delegateSel, sectionCtrl, media);
|
||||||
|
}
|
||||||
|
if (sectionCtrl) {
|
||||||
|
SEL markSel = NSSelectorFromString(@"markItemAsSeen:");
|
||||||
|
if ([sectionCtrl respondsToSelector:markSel])
|
||||||
|
((SCIMsgSend1)objc_msgSend)(sectionCtrl, markSel, media);
|
||||||
|
}
|
||||||
|
id seenManager = sciCall(storyVC, @selector(viewingSessionSeenStateManager));
|
||||||
|
id vm = sciCall(storyVC, @selector(currentViewModel));
|
||||||
|
if (seenManager && vm) {
|
||||||
|
SEL setSel = NSSelectorFromString(@"setSeenMediaId:forReelPK:");
|
||||||
|
if ([seenManager respondsToSelector:setSel]) {
|
||||||
|
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, setSel, mediaPK, reelPK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sciSeenBypassActive = NO;
|
||||||
|
[SCIUtils showToastForDuration:2.0 title:@"Marked as seen" subtitle:@"Will sync when leaving stories"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DM visual message path
|
||||||
|
UIViewController *dmVC = sciFindVC(self, @"IGDirectVisualMessageViewerController");
|
||||||
|
if (dmVC) {
|
||||||
|
extern BOOL dmVisualMsgsViewedButtonEnabled;
|
||||||
|
BOOL wasEnabled = dmVisualMsgsViewedButtonEnabled;
|
||||||
|
dmVisualMsgsViewedButtonEnabled = YES;
|
||||||
|
|
||||||
|
Ivar dsIvar = class_getInstanceVariable([dmVC class], "_dataSource");
|
||||||
|
id ds = dsIvar ? object_getIvar(dmVC, dsIvar) : nil;
|
||||||
|
Ivar msgIvar = ds ? class_getInstanceVariable([ds class], "_currentMessage") : nil;
|
||||||
|
id msg = msgIvar ? object_getIvar(ds, msgIvar) : nil;
|
||||||
|
Ivar erIvar = class_getInstanceVariable([dmVC class], "_eventResponders");
|
||||||
|
NSArray *responders = erIvar ? object_getIvar(dmVC, erIvar) : nil;
|
||||||
|
|
||||||
|
if (responders && msg) {
|
||||||
|
for (id resp in responders) {
|
||||||
|
SEL beginSel = @selector(visualMessageViewerController:didBeginPlaybackForVisualMessage:atIndex:);
|
||||||
|
if ([resp respondsToSelector:beginSel]) {
|
||||||
|
typedef void (*Fn)(id, SEL, id, id, NSInteger);
|
||||||
|
((Fn)objc_msgSend)(resp, beginSel, dmVC, msg, 0);
|
||||||
|
}
|
||||||
|
SEL endSel = @selector(visualMessageViewerController:didEndPlaybackForVisualMessage:atIndex:mediaCurrentTime:forNavType:);
|
||||||
|
if ([resp respondsToSelector:endSel]) {
|
||||||
|
typedef void (*Fn)(id, SEL, id, id, NSInteger, CGFloat, NSInteger);
|
||||||
|
((Fn)objc_msgSend)(resp, endSel, dmVC, msg, 0, 0.0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SEL dismissSel = NSSelectorFromString(@"_didTapHeaderViewDismissButton:");
|
||||||
|
if ([dmVC respondsToSelector:dismissSel])
|
||||||
|
((void(*)(id,SEL,id))objc_msgSend)(dmVC, dismissSel, nil);
|
||||||
|
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
|
dmVisualMsgsViewedButtonEnabled = wasEnabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
[SCIUtils showToastForDuration:1.5 title:@"Marked as viewed"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SCIUtils showErrorHUDWithDescription:@"VC not found"];
|
||||||
|
} @catch (NSException *e) {
|
||||||
|
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:@"Error: %@", e.reason]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
%end
|
||||||
@@ -75,22 +75,14 @@
|
|||||||
}
|
}
|
||||||
%end
|
%end
|
||||||
|
|
||||||
// DM stories viewed logic
|
// DM visual messages viewed logic
|
||||||
%hook IGDirectVisualMessageViewerEventHandler
|
%hook IGDirectVisualMessageViewerEventHandler
|
||||||
- (void)visualMessageViewerController:(id)arg1 didBeginPlaybackForVisualMessage:(id)arg2 atIndex:(NSInteger)arg3 {
|
- (void)visualMessageViewerController:(id)arg1 didBeginPlaybackForVisualMessage:(id)arg2 atIndex:(NSInteger)arg3 {
|
||||||
if ([SCIUtils getBoolPref:@"unlimited_replay"]) {
|
if ([SCIUtils getBoolPref:@"unlimited_replay"] && !dmVisualMsgsViewedButtonEnabled) return;
|
||||||
// Check if dm stories should be marked as viewed
|
%orig;
|
||||||
if (dmVisualMsgsViewedButtonEnabled) {
|
|
||||||
%orig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
- (void)visualMessageViewerController:(id)arg1 didEndPlaybackForVisualMessage:(id)arg2 atIndex:(NSInteger)arg3 mediaCurrentTime:(CGFloat)arg4 forNavType:(NSInteger)arg5 {
|
- (void)visualMessageViewerController:(id)arg1 didEndPlaybackForVisualMessage:(id)arg2 atIndex:(NSInteger)arg3 mediaCurrentTime:(CGFloat)arg4 forNavType:(NSInteger)arg5 {
|
||||||
if ([SCIUtils getBoolPref:@"unlimited_replay"]) {
|
if ([SCIUtils getBoolPref:@"unlimited_replay"] && !dmVisualMsgsViewedButtonEnabled) return;
|
||||||
// Check if dm stories should be marked as viewed
|
%orig;
|
||||||
if (dmVisualMsgsViewedButtonEnabled) {
|
|
||||||
%orig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
%end
|
%end
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Shared helpers for story/DM visual message features
|
||||||
|
#import "../../Utils.h"
|
||||||
|
#import "../../InstagramHeaders.h"
|
||||||
|
#import "../../Downloader/Download.h"
|
||||||
|
#import <objc/runtime.h>
|
||||||
|
#import <objc/message.h>
|
||||||
|
|
||||||
|
typedef id (*SCIMsgSend)(id, SEL);
|
||||||
|
typedef id (*SCIMsgSend1)(id, SEL, id);
|
||||||
|
|
||||||
|
static inline id sciCall(id obj, SEL sel) {
|
||||||
|
if (!obj || ![obj respondsToSelector:sel]) return nil;
|
||||||
|
return ((SCIMsgSend)objc_msgSend)(obj, sel);
|
||||||
|
}
|
||||||
|
static inline id sciCall1(id obj, SEL sel, id arg1) {
|
||||||
|
if (!obj || ![obj respondsToSelector:sel]) return nil;
|
||||||
|
return ((SCIMsgSend1)objc_msgSend)(obj, sel, arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline 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 inline 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 inline 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline id _Nullable sciFindSectionController(UIViewController *storyVC) {
|
||||||
|
Class sectionClass = NSClassFromString(@"IGStoryFullscreenSectionController");
|
||||||
|
if (!sectionClass || !storyVC) return nil;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
+48
-13
@@ -13,7 +13,7 @@
|
|||||||
///////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// * Tweak version *
|
// * Tweak version *
|
||||||
NSString *SCIVersionString = @"v1.1.4";
|
NSString *SCIVersionString = @"v1.1.5";
|
||||||
|
|
||||||
// Variables that work across features
|
// Variables that work across features
|
||||||
BOOL dmVisualMsgsViewedButtonEnabled = false;
|
BOOL dmVisualMsgsViewedButtonEnabled = false;
|
||||||
@@ -90,10 +90,11 @@ BOOL dmVisualMsgsViewedButtonEnabled = false;
|
|||||||
|
|
||||||
- (void)applicationDidBecomeActive:(id)arg1 {
|
- (void)applicationDidBecomeActive:(id)arg1 {
|
||||||
%orig;
|
%orig;
|
||||||
|
|
||||||
if ([SCIUtils getBoolPref:@"flex_app_start"]) {
|
if ([SCIUtils getBoolPref:@"flex_app_start"]) {
|
||||||
[[objc_getClass("FLEXManager") sharedManager] showExplorer];
|
[[objc_getClass("FLEXManager") sharedManager] showExplorer];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
%end
|
%end
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ BOOL dmVisualMsgsViewedButtonEnabled = false;
|
|||||||
- (_Bool)isLiquidGlassInAppNotificationEnabled {
|
- (_Bool)isLiquidGlassInAppNotificationEnabled {
|
||||||
return [SCIUtils liquidGlassEnabledBool:%orig];
|
return [SCIUtils liquidGlassEnabledBool:%orig];
|
||||||
}
|
}
|
||||||
- (_Bool)isLiquidGlassContextMenuEnabled{
|
- (_Bool)isLiquidGlassContextMenuEnabled {
|
||||||
return [SCIUtils liquidGlassEnabledBool:%orig];
|
return [SCIUtils liquidGlassEnabledBool:%orig];
|
||||||
}
|
}
|
||||||
- (_Bool)isLiquidGlassToastEnabled {
|
- (_Bool)isLiquidGlassToastEnabled {
|
||||||
@@ -113,8 +114,12 @@ BOOL dmVisualMsgsViewedButtonEnabled = false;
|
|||||||
- (_Bool)isLiquidGlassAlertDialogEnabled {
|
- (_Bool)isLiquidGlassAlertDialogEnabled {
|
||||||
return [SCIUtils liquidGlassEnabledBool:%orig];
|
return [SCIUtils liquidGlassEnabledBool:%orig];
|
||||||
}
|
}
|
||||||
|
- (_Bool)isLiquidGlassIconBarButtonEnabled {
|
||||||
|
return [SCIUtils liquidGlassEnabledBool:%orig];
|
||||||
|
}
|
||||||
%end
|
%end
|
||||||
|
|
||||||
|
|
||||||
// Disable sending modded insta bug reports
|
// Disable sending modded insta bug reports
|
||||||
%hook IGWindow
|
%hook IGWindow
|
||||||
- (void)showDebugMenu {
|
- (void)showDebugMenu {
|
||||||
@@ -190,6 +195,7 @@ shouldPersistLastBugReportId:(id)arg6
|
|||||||
%hook IGDirectVisualMessageViewerController
|
%hook IGDirectVisualMessageViewerController
|
||||||
- (void)screenshotObserverDidSeeScreenshotTaken:(id)arg1 { VOID_HANDLESCREENSHOT(%orig); }
|
- (void)screenshotObserverDidSeeScreenshotTaken:(id)arg1 { VOID_HANDLESCREENSHOT(%orig); }
|
||||||
- (void)screenshotObserverDidSeeActiveScreenCapture:(id)arg1 event:(NSInteger)arg2 { VOID_HANDLESCREENSHOT(%orig); }
|
- (void)screenshotObserverDidSeeActiveScreenCapture:(id)arg1 event:(NSInteger)arg2 { VOID_HANDLESCREENSHOT(%orig); }
|
||||||
|
|
||||||
%end
|
%end
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -659,24 +665,18 @@ shouldPersistLastBugReportId:(id)arg6
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)_didTapRepostButton:(id)arg1 {
|
- (void)_didTapRepostButton {
|
||||||
if ([SCIUtils getBoolPref:@"repost_confirm"]) {
|
if ([SCIUtils getBoolPref:@"repost_confirm"]) {
|
||||||
NSLog(@"[SCInsta] Confirm repost triggered");
|
|
||||||
|
|
||||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return %orig;
|
%orig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)_didLongPressRepostButton:(id)arg1 {
|
- (void)_didLongPressRepostButton:(id)arg1 {
|
||||||
if ([SCIUtils getBoolPref:@"repost_confirm"]) {
|
if ([SCIUtils getBoolPref:@"repost_confirm"]) return;
|
||||||
NSLog(@"[SCInsta] Confirm repost triggered (long press ignored)");
|
%orig;
|
||||||
}
|
|
||||||
else {
|
|
||||||
return %orig;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
%end
|
%end
|
||||||
|
|
||||||
@@ -717,3 +717,38 @@ shouldPersistLastBugReportId:(id)arg6
|
|||||||
return %orig;
|
return %orig;
|
||||||
}
|
}
|
||||||
%end
|
%end
|
||||||
|
|
||||||
|
// liquid glass Swift class hooks
|
||||||
|
static BOOL (*orig_swizzleToggle_isEnabled)(id, SEL) = NULL;
|
||||||
|
static BOOL new_swizzleToggle_isEnabled(id self, SEL _cmd) {
|
||||||
|
if ([SCIUtils getBoolPref:@"liquid_glass_buttons"]) return YES;
|
||||||
|
return orig_swizzleToggle_isEnabled(self, _cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL (*orig_expHelper_isEnabled)(id, SEL) = NULL;
|
||||||
|
static BOOL new_expHelper_isEnabled(id self, SEL _cmd) {
|
||||||
|
if ([SCIUtils getBoolPref:@"liquid_glass_buttons"]) return YES;
|
||||||
|
return orig_expHelper_isEnabled(self, _cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL (*orig_expHelper_isHomeFeed)(id, SEL) = NULL;
|
||||||
|
static BOOL new_expHelper_isHomeFeed(id self, SEL _cmd) {
|
||||||
|
if ([SCIUtils getBoolPref:@"liquid_glass_buttons"]) return YES;
|
||||||
|
return orig_expHelper_isHomeFeed(self, _cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
%ctor {
|
||||||
|
Class swizzleToggle = objc_getClass("IGLiquidGlassSwizzle.IGLiquidGlassSwizzleToggle");
|
||||||
|
if (swizzleToggle) {
|
||||||
|
MSHookMessageEx(swizzleToggle, @selector(isEnabled),
|
||||||
|
(IMP)new_swizzleToggle_isEnabled, (IMP *)&orig_swizzleToggle_isEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
Class expHelper = objc_getClass("IGLiquidGlassExperimentHelper.IGLiquidGlassNavigationExperimentHelper");
|
||||||
|
if (expHelper) {
|
||||||
|
MSHookMessageEx(expHelper, @selector(isEnabled),
|
||||||
|
(IMP)new_expHelper_isEnabled, (IMP *)&orig_expHelper_isEnabled);
|
||||||
|
MSHookMessageEx(expHelper, @selector(isHomeFeedHeaderEnabled),
|
||||||
|
(IMP)new_expHelper_isHomeFeed, (IMP *)&orig_expHelper_isHomeFeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user