mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-07-30 16:08:57 +02:00
feat: Mark as seen on typing
feat: Save to RyukGram album feat: Hide trailing action buttons on preserved messages feat: Confirmation when pulling to refresh the DMs tab if preserved messages would be cleared
This commit is contained in:
@@ -1,7 +1,20 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
// Defined in SeenButtons.x
|
||||
extern __weak IGDirectThreadViewController *sciActiveThreadVC;
|
||||
extern BOOL sciAutoTypingEnabled(void);
|
||||
extern void sciDoAutoSeen(IGDirectThreadViewController *threadVC);
|
||||
|
||||
%hook IGDirectTypingStatusService
|
||||
- (void)updateOutgoingStatusIsActive:(_Bool)active threadKey:(id)key threadMetadata:(id)metadata typingStatusType:(long long)type {
|
||||
// Mark the visible thread as seen on the first typing event — runs even
|
||||
// when typing-status broadcasting is blocked below.
|
||||
if (active && sciAutoTypingEnabled()) {
|
||||
IGDirectThreadViewController *vc = sciActiveThreadVC;
|
||||
if (vc) sciDoAutoSeen(vc);
|
||||
}
|
||||
|
||||
if ([SCIUtils getBoolPref:@"disable_typing_status"]) return;
|
||||
|
||||
return %orig(active, key, metadata, type);
|
||||
|
||||
@@ -140,13 +140,7 @@ static id new_prismMenuView_init3(id self, SEL _cmd, NSArray *elements, id heade
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[pill setText:@"Done!"];
|
||||
[pill dismissAfterDelay:0.5];
|
||||
|
||||
UIActivityViewController *shareVC = [[UIActivityViewController alloc]
|
||||
initWithActivityItems:@[finalURL]
|
||||
applicationActivities:nil];
|
||||
UIViewController *top = [UIApplication sharedApplication].keyWindow.rootViewController;
|
||||
while (top.presentedViewController) top = top.presentedViewController;
|
||||
[top presentViewController:shareVC animated:YES completion:nil];
|
||||
[SCIUtils showShareVC:finalURL];
|
||||
});
|
||||
}];
|
||||
}];
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Pull-to-refresh in the DMs tab silently clears preserved (locally retained)
|
||||
// unsent messages. This hook intercepts _pullToRefreshIfPossible to show a
|
||||
// confirmation dialog when both keep_deleted_message and
|
||||
// warn_refresh_clears_preserved are on.
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import <objc/runtime.h>
|
||||
#import <objc/message.h>
|
||||
#import <substrate.h>
|
||||
|
||||
extern NSMutableSet *sciGetPreservedIds(void);
|
||||
extern void sciClearPreservedIds(void);
|
||||
|
||||
static BOOL sciRefreshConfirmInFlight = NO;
|
||||
static BOOL sciRefreshAlertVisible = NO;
|
||||
|
||||
static UIRefreshControl *sciFindRefreshControl(UIViewController *vc) {
|
||||
Class igRC = NSClassFromString(@"IGRefreshControl");
|
||||
NSMutableArray *stack = [NSMutableArray arrayWithObject:vc.view];
|
||||
while (stack.count > 0) {
|
||||
UIView *v = stack.lastObject;
|
||||
[stack removeLastObject];
|
||||
if ((igRC && [v isKindOfClass:igRC]) || [v isKindOfClass:[UIRefreshControl class]]) {
|
||||
return (UIRefreshControl *)v;
|
||||
}
|
||||
for (UIView *sub in v.subviews) [stack addObject:sub];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// On cancel, the IGRefreshControl's state machine is already idle by the time
|
||||
// our handler runs — but the scroll view's contentInset stays expanded, leaving
|
||||
// the spinner area visually exposed. We grab the idle inset via the inbox VC's
|
||||
// idleTopContentInsetForRefreshControl: helper and animate the inset back.
|
||||
static void sciCancelRefresh(UIViewController *vc) {
|
||||
UIRefreshControl *rc = sciFindRefreshControl(vc);
|
||||
if (!rc) return;
|
||||
|
||||
Ivar stateIvar = class_getInstanceVariable([rc class], "_refreshState");
|
||||
if (stateIvar) {
|
||||
ptrdiff_t off = ivar_getOffset(stateIvar);
|
||||
*(NSInteger *)((char *)(__bridge void *)rc + off) = 0;
|
||||
}
|
||||
Ivar animIvar = class_getInstanceVariable([rc class], "_swiftAnimationInfo");
|
||||
if (animIvar) object_setIvar(rc, animIvar, nil);
|
||||
if ([rc respondsToSelector:@selector(endRefreshing)]) [rc endRefreshing];
|
||||
|
||||
SEL didEnd = NSSelectorFromString(@"refreshControlDidEndFinishLoadingAnimation:");
|
||||
if ([vc respondsToSelector:didEnd]) {
|
||||
((void(*)(id, SEL, id))objc_msgSend)(vc, didEnd, rc);
|
||||
}
|
||||
|
||||
UIScrollView *scroll = nil;
|
||||
UIView *cur = rc.superview;
|
||||
while (cur) {
|
||||
if ([cur isKindOfClass:[UIScrollView class]]) { scroll = (UIScrollView *)cur; break; }
|
||||
cur = cur.superview;
|
||||
}
|
||||
if (scroll) {
|
||||
SEL idleSel = NSSelectorFromString(@"idleTopContentInsetForRefreshControl:");
|
||||
CGFloat idleInset = scroll.contentInset.top;
|
||||
if ([vc respondsToSelector:idleSel]) {
|
||||
idleInset = ((CGFloat(*)(id, SEL, id))objc_msgSend)(vc, idleSel, rc);
|
||||
}
|
||||
UIEdgeInsets insets = scroll.contentInset;
|
||||
insets.top = idleInset;
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
scroll.contentInset = insets;
|
||||
CGPoint o = scroll.contentOffset;
|
||||
if (o.y < -idleInset) o.y = -idleInset;
|
||||
scroll.contentOffset = o;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
static void (*orig_pullToRefresh)(id self, SEL _cmd);
|
||||
static void new_pullToRefresh(id self, SEL _cmd) {
|
||||
if (sciRefreshConfirmInFlight ||
|
||||
![SCIUtils getBoolPref:@"keep_deleted_message"] ||
|
||||
![SCIUtils getBoolPref:@"warn_refresh_clears_preserved"]) {
|
||||
orig_pullToRefresh(self, _cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
// IG fires _pullToRefreshIfPossible repeatedly while the user holds the
|
||||
// pull gesture — drop re-entrant calls until the alert is dismissed.
|
||||
if (sciRefreshAlertVisible) return;
|
||||
|
||||
NSUInteger count = sciGetPreservedIds().count;
|
||||
if (count == 0) {
|
||||
orig_pullToRefresh(self, _cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
UIViewController *vc = (UIViewController *)self;
|
||||
NSString *msg = [NSString stringWithFormat:
|
||||
@"Refreshing the DMs tab will clear %lu preserved unsent message%@. This cannot be undone.",
|
||||
(unsigned long)count, count == 1 ? @"" : @"s"];
|
||||
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Clear preserved messages?"
|
||||
message:msg
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
__weak UIViewController *weakSelf = vc;
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *a) {
|
||||
sciCancelRefresh(weakSelf);
|
||||
sciRefreshAlertVisible = NO;
|
||||
}]];
|
||||
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Refresh" style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction *a) {
|
||||
sciRefreshAlertVisible = NO;
|
||||
id strongSelf = weakSelf;
|
||||
if (!strongSelf) return;
|
||||
sciClearPreservedIds();
|
||||
sciRefreshConfirmInFlight = YES;
|
||||
((void(*)(id, SEL))objc_msgSend)(strongSelf, _cmd);
|
||||
sciRefreshConfirmInFlight = NO;
|
||||
}]];
|
||||
|
||||
sciRefreshAlertVisible = YES;
|
||||
UIViewController *top = [UIApplication sharedApplication].keyWindow.rootViewController;
|
||||
while (top.presentedViewController) top = top.presentedViewController;
|
||||
[top presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
%ctor {
|
||||
Class cls = NSClassFromString(@"IGDirectInboxViewController");
|
||||
if (!cls) return;
|
||||
SEL sel = NSSelectorFromString(@"_pullToRefreshIfPossible");
|
||||
if (class_getInstanceMethod(cls, sel))
|
||||
MSHookMessageEx(cls, sel, (IMP)new_pullToRefresh, (IMP *)&orig_pullToRefresh);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ static NSMutableSet *sciPreservedIds = nil;
|
||||
#define SCI_PRESERVED_MAX 200
|
||||
#define SCI_PRESERVED_TAG 1399
|
||||
|
||||
static NSMutableSet *sciGetPreservedIds() {
|
||||
NSMutableSet *sciGetPreservedIds() {
|
||||
if (!sciPreservedIds) {
|
||||
NSArray *saved = [[NSUserDefaults standardUserDefaults] arrayForKey:SCI_PRESERVED_IDS_KEY];
|
||||
sciPreservedIds = saved ? [NSMutableSet setWithArray:saved] : [NSMutableSet set];
|
||||
@@ -48,7 +48,7 @@ static void sciSavePreservedIds() {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:[ids allObjects] forKey:SCI_PRESERVED_IDS_KEY];
|
||||
}
|
||||
|
||||
static void sciClearPreservedIds() {
|
||||
void sciClearPreservedIds() {
|
||||
[sciGetPreservedIds() removeAllObjects];
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:SCI_PRESERVED_IDS_KEY];
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
BOOL dmSeenToggleEnabled = NO;
|
||||
static BOOL sciSeenAutoBypass = NO;
|
||||
__weak IGDirectThreadViewController *sciActiveThreadVC = nil;
|
||||
|
||||
static BOOL sciIsSeenToggleMode() {
|
||||
return [[SCIUtils getStringPref:@"seen_mode"] isEqualToString:@"toggle"];
|
||||
@@ -21,7 +22,11 @@ static BOOL sciAutoInteractEnabled() {
|
||||
return [SCIUtils getBoolPref:@"remove_lastseen"] && [SCIUtils getBoolPref:@"seen_auto_on_interact"];
|
||||
}
|
||||
|
||||
static void sciDoAutoSeen(IGDirectThreadViewController *threadVC) {
|
||||
BOOL sciAutoTypingEnabled() {
|
||||
return [SCIUtils getBoolPref:@"remove_lastseen"] && [SCIUtils getBoolPref:@"seen_auto_on_typing"];
|
||||
}
|
||||
|
||||
void sciDoAutoSeen(IGDirectThreadViewController *threadVC) {
|
||||
sciSeenAutoBypass = YES;
|
||||
[threadVC markLastMessageAsSeen];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
@@ -40,6 +45,21 @@ static void new_setHasSent(id self, SEL _cmd, BOOL sent) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============ AUTO SEEN ON TYPING ============
|
||||
// Tracks the visible thread VC so the typing-service hook (in
|
||||
// DisableTypingStatus.x) can mark its messages as seen.
|
||||
|
||||
%hook IGDirectThreadViewController
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
%orig;
|
||||
sciActiveThreadVC = self;
|
||||
}
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
if (sciActiveThreadVC == self) sciActiveThreadVC = nil;
|
||||
%orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// ============ NAV BAR BUTTONS ============
|
||||
|
||||
%hook IGTallNavigationBarView
|
||||
|
||||
Reference in New Issue
Block a user