mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-05-01 08:37:57 +02:00
- Fixed keep deleted messages
- Unsent message indicator: visual "Unsent" label on preserved messages - Unsent message notification pill when a message is preserved - Reorganized DM settings into sub-pages (keep deleted messages, read receipts)
This commit is contained in:
@@ -78,9 +78,10 @@ A feature-rich iOS tweak for Instagram, forked from [SCInsta](https://github.com
|
||||
- *Customize hold time for long-press*
|
||||
|
||||
### Stories and messages
|
||||
- Keep deleted messages
|
||||
- Keep deleted messages (preserves unsent messages with visual indicator and notification pill) **\***
|
||||
- Manually mark messages as seen (button or toggle mode) **\***
|
||||
- Auto mark seen on send (marks messages as read when you send any message) **\***
|
||||
- Send audio as file — send audio files as voice messages from the DM plus menu **\***
|
||||
- Disable typing status
|
||||
- Unlimited replay of direct stories
|
||||
- Disable view-once limitations
|
||||
@@ -120,6 +121,9 @@ A feature-rich iOS tweak for Instagram, forked from [SCInsta](https://github.com
|
||||
### Optimization
|
||||
- Automatically clears unneeded cache folders, reducing the size of your Instagram installation
|
||||
|
||||
## Known Issues
|
||||
- Preserved unsent messages cannot be removed using "Delete for you". Pull to refresh in the DMs tab clears all preserved messages as a workaround.
|
||||
|
||||
# Opening Tweak Settings
|
||||
|
||||
| | |
|
||||
|
||||
@@ -1,22 +1,302 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import <objc/runtime.h>
|
||||
#import <substrate.h>
|
||||
|
||||
%hook IGDirectRealtimeIrisThreadDelta
|
||||
+ (id)removeItemWithMessageId:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"keep_deleted_message"]) {
|
||||
arg1 = NULL;
|
||||
// ============ KEEP DELETED MESSAGES ============
|
||||
// Blocks remote unsends while allowing local deletes.
|
||||
// IGDirectMessageUpdate._removeMessages_reason: 0 = unsend, 2 = delete-for-you.
|
||||
// Delete-for-you fires reason=2 then reason=0 follow-up — tracked with a counter.
|
||||
// Remote unsend fires only reason=0 — blocked when counter is 0 and not local.
|
||||
|
||||
static BOOL sciKeepDeletedEnabled() {
|
||||
return [SCIUtils getBoolPref:@"keep_deleted_message"];
|
||||
}
|
||||
|
||||
static BOOL sciIndicateUnsentEnabled() {
|
||||
return [SCIUtils getBoolPref:@"indicate_unsent_messages"];
|
||||
}
|
||||
|
||||
static void sciUpdateCellIndicator(id cell);
|
||||
static BOOL sciLocalDeleteInProgress = NO;
|
||||
static NSMutableArray *sciPendingUpdates = nil;
|
||||
static NSInteger sciDeleteForYouCount = 0;
|
||||
static NSMutableSet *sciPreservedIds = nil;
|
||||
|
||||
#define SCI_PRESERVED_IDS_KEY @"SCIPreservedMsgIds"
|
||||
#define SCI_PRESERVED_MAX 200
|
||||
#define SCI_PRESERVED_TAG 1399
|
||||
|
||||
static NSMutableSet *sciGetPreservedIds() {
|
||||
if (!sciPreservedIds) {
|
||||
NSArray *saved = [[NSUserDefaults standardUserDefaults] arrayForKey:SCI_PRESERVED_IDS_KEY];
|
||||
sciPreservedIds = saved ? [NSMutableSet setWithArray:saved] : [NSMutableSet set];
|
||||
}
|
||||
return sciPreservedIds;
|
||||
}
|
||||
|
||||
static void sciSavePreservedIds() {
|
||||
NSMutableSet *ids = sciGetPreservedIds();
|
||||
while (ids.count > SCI_PRESERVED_MAX)
|
||||
[ids removeObject:[ids anyObject]];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:[ids allObjects] forKey:SCI_PRESERVED_IDS_KEY];
|
||||
}
|
||||
|
||||
static void sciClearPreservedIds() {
|
||||
[sciGetPreservedIds() removeAllObjects];
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:SCI_PRESERVED_IDS_KEY];
|
||||
}
|
||||
|
||||
// ============ ALLOC TRACKING ============
|
||||
|
||||
static id (*orig_msgUpdate_alloc)(id self, SEL _cmd);
|
||||
static id new_msgUpdate_alloc(id self, SEL _cmd) {
|
||||
id instance = orig_msgUpdate_alloc(self, _cmd);
|
||||
if (sciKeepDeletedEnabled() && instance) {
|
||||
if (!sciPendingUpdates) sciPendingUpdates = [NSMutableArray array];
|
||||
@synchronized(sciPendingUpdates) {
|
||||
[sciPendingUpdates addObject:instance];
|
||||
while (sciPendingUpdates.count > 10)
|
||||
[sciPendingUpdates removeObjectAtIndex:0];
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ============ REMOTE UNSEND DETECTION ============
|
||||
|
||||
static BOOL sciConsumeRemoteUnsend() {
|
||||
if (!sciPendingUpdates) return NO;
|
||||
|
||||
BOOL shouldBlock = NO;
|
||||
@synchronized(sciPendingUpdates) {
|
||||
for (id update in [sciPendingUpdates copy]) {
|
||||
@try {
|
||||
Ivar removeIvar = class_getInstanceVariable([update class], "_removeMessages_messageKeys");
|
||||
if (!removeIvar) continue;
|
||||
NSArray *keys = object_getIvar(update, removeIvar);
|
||||
if (!keys || keys.count == 0) continue;
|
||||
|
||||
long long reason = -1;
|
||||
Ivar reasonIvar = class_getInstanceVariable([update class], "_removeMessages_reason");
|
||||
if (reasonIvar) {
|
||||
ptrdiff_t off = ivar_getOffset(reasonIvar);
|
||||
reason = *(long long *)((char *)(__bridge void *)update + off);
|
||||
}
|
||||
|
||||
if (reason == 2) {
|
||||
sciDeleteForYouCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reason == 0 && !sciLocalDeleteInProgress) {
|
||||
if (sciDeleteForYouCount > 0) {
|
||||
sciDeleteForYouCount--;
|
||||
continue;
|
||||
}
|
||||
for (id key in keys) {
|
||||
Ivar sidIvar = class_getInstanceVariable([key class], "_messageServerId");
|
||||
if (sidIvar) {
|
||||
NSString *sid = object_getIvar(key, sidIvar);
|
||||
if ([sid isKindOfClass:[NSString class]] && sid.length > 0)
|
||||
[sciGetPreservedIds() addObject:sid];
|
||||
}
|
||||
}
|
||||
sciSavePreservedIds();
|
||||
shouldBlock = YES;
|
||||
break;
|
||||
}
|
||||
} @catch(id e) {}
|
||||
}
|
||||
[sciPendingUpdates removeAllObjects];
|
||||
}
|
||||
return shouldBlock;
|
||||
}
|
||||
|
||||
// ============ 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()) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Update visible cell indicators
|
||||
Class cellClass = NSClassFromString(@"IGDirectMessageCell");
|
||||
if (cellClass) {
|
||||
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||
NSMutableArray *stack = [NSMutableArray arrayWithObject:window];
|
||||
while (stack.count > 0) {
|
||||
UIView *v = stack.lastObject;
|
||||
[stack removeLastObject];
|
||||
if ([v isKindOfClass:cellClass]) {
|
||||
sciUpdateCellIndicator(v);
|
||||
continue;
|
||||
}
|
||||
for (UIView *sub in v.subviews)
|
||||
[stack addObject:sub];
|
||||
}
|
||||
}
|
||||
|
||||
// Show pill notification
|
||||
if ([SCIUtils getBoolPref:@"unsent_message_toast"]) {
|
||||
UIView *hostView = [UIApplication sharedApplication].keyWindow;
|
||||
if (hostView) {
|
||||
UIView *pill = [[UIView alloc] init];
|
||||
pill.backgroundColor = [UIColor colorWithRed:0.85 green:0.15 blue:0.15 alpha:0.95];
|
||||
pill.layer.cornerRadius = 18;
|
||||
pill.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
pill.layer.shadowOpacity = 0.4;
|
||||
pill.layer.shadowOffset = CGSizeMake(0, 2);
|
||||
pill.layer.shadowRadius = 8;
|
||||
pill.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
pill.alpha = 0;
|
||||
|
||||
UILabel *label = [[UILabel alloc] init];
|
||||
label.text = @"A message was unsent";
|
||||
label.textColor = [UIColor whiteColor];
|
||||
label.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
|
||||
label.textAlignment = NSTextAlignmentCenter;
|
||||
label.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[pill addSubview:label];
|
||||
|
||||
[hostView addSubview:pill];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[pill.topAnchor constraintEqualToAnchor:hostView.safeAreaLayoutGuide.topAnchor constant:8],
|
||||
[pill.centerXAnchor constraintEqualToAnchor:hostView.centerXAnchor],
|
||||
[pill.heightAnchor constraintEqualToConstant:36],
|
||||
[label.centerXAnchor constraintEqualToAnchor:pill.centerXAnchor],
|
||||
[label.centerYAnchor constraintEqualToAnchor:pill.centerYAnchor],
|
||||
[label.leadingAnchor constraintEqualToAnchor:pill.leadingAnchor constant:20],
|
||||
[label.trailingAnchor constraintEqualToAnchor:pill.trailingAnchor constant:-20],
|
||||
]];
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^{ pill.alpha = 1; }];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[UIView animateWithDuration:0.3 animations:^{ pill.alpha = 0; } completion:^(BOOL f) {
|
||||
[pill removeFromSuperview];
|
||||
}];
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
orig_applyUpdates(self, _cmd, updates, completion, userAccess);
|
||||
}
|
||||
|
||||
// ============ LOCAL DELETE TRACKING ============
|
||||
|
||||
static void (*orig_removeMutation_execute)(id self, SEL _cmd, id handler, id pkg);
|
||||
static void new_removeMutation_execute(id self, SEL _cmd, id handler, id pkg) {
|
||||
sciLocalDeleteInProgress = YES;
|
||||
orig_removeMutation_execute(self, _cmd, handler, pkg);
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
sciLocalDeleteInProgress = NO;
|
||||
});
|
||||
}
|
||||
|
||||
// ============ VISUAL INDICATOR ============
|
||||
|
||||
static NSString * _Nullable sciGetCellServerId(id cell) {
|
||||
@try {
|
||||
Ivar vmIvar = class_getInstanceVariable([cell class], "_viewModel");
|
||||
if (!vmIvar) return nil;
|
||||
id vm = object_getIvar(cell, vmIvar);
|
||||
if (!vm) return nil;
|
||||
|
||||
SEL metaSel = NSSelectorFromString(@"messageMetadata");
|
||||
if (![vm respondsToSelector:metaSel]) return nil;
|
||||
id meta = ((id(*)(id,SEL))objc_msgSend)(vm, metaSel);
|
||||
if (!meta) return nil;
|
||||
|
||||
Ivar keyIvar = class_getInstanceVariable([meta class], "_key");
|
||||
if (!keyIvar) return nil;
|
||||
id keyObj = object_getIvar(meta, keyIvar);
|
||||
if (!keyObj) return nil;
|
||||
|
||||
Ivar sidIvar = class_getInstanceVariable([keyObj class], "_serverId");
|
||||
if (!sidIvar) return nil;
|
||||
NSString *serverId = object_getIvar(keyObj, sidIvar);
|
||||
return [serverId isKindOfClass:[NSString class]] ? serverId : nil;
|
||||
} @catch(id e) {}
|
||||
return nil;
|
||||
}
|
||||
|
||||
static void sciUpdateCellIndicator(id cell) {
|
||||
UIView *view = (UIView *)cell;
|
||||
UIView *oldIndicator = [view viewWithTag:SCI_PRESERVED_TAG];
|
||||
|
||||
if (!sciIndicateUnsentEnabled()) {
|
||||
if (oldIndicator) [oldIndicator removeFromSuperview];
|
||||
return;
|
||||
}
|
||||
|
||||
return %orig(arg1);
|
||||
}
|
||||
%end
|
||||
NSString *serverId = sciGetCellServerId(cell);
|
||||
BOOL isPreserved = serverId && [sciGetPreservedIds() containsObject:serverId];
|
||||
|
||||
%hook IGDirectMessageUpdate
|
||||
+ (id)removeMessageWithMessageId:(id)arg1{
|
||||
if ([SCIUtils getBoolPref:@"keep_deleted_message"]) {
|
||||
arg1 = NULL;
|
||||
if (isPreserved) {
|
||||
if (!oldIndicator) {
|
||||
Ivar bubbleIvar = class_getInstanceVariable([cell class], "_messageContentContainerView");
|
||||
UIView *bubble = bubbleIvar ? object_getIvar(cell, bubbleIvar) : nil;
|
||||
UIView *anchor = bubble ?: view;
|
||||
|
||||
UILabel *label = [[UILabel alloc] init];
|
||||
label.tag = SCI_PRESERVED_TAG;
|
||||
label.text = @"Unsent";
|
||||
label.font = [UIFont italicSystemFontOfSize:10];
|
||||
label.textColor = [UIColor colorWithRed:1.0 green:0.3 blue:0.3 alpha:0.9];
|
||||
label.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[view addSubview:label];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[label.leadingAnchor constraintEqualToAnchor:anchor.trailingAnchor constant:4],
|
||||
[label.centerYAnchor constraintEqualToAnchor:anchor.centerYAnchor],
|
||||
]];
|
||||
}
|
||||
} else if (oldIndicator) {
|
||||
[oldIndicator removeFromSuperview];
|
||||
}
|
||||
}
|
||||
|
||||
static void (*orig_configureCell)(id self, SEL _cmd, id vm, id ringSpec, id launcherSet);
|
||||
static void new_configureCell(id self, SEL _cmd, id vm, id ringSpec, id launcherSet) {
|
||||
orig_configureCell(self, _cmd, vm, ringSpec, launcherSet);
|
||||
sciUpdateCellIndicator(self);
|
||||
}
|
||||
|
||||
// ============ RUNTIME HOOKS ============
|
||||
|
||||
%ctor {
|
||||
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:");
|
||||
if (class_getInstanceMethod(cacheClass, sel))
|
||||
MSHookMessageEx(cacheClass, sel, (IMP)new_applyUpdates, (IMP *)&orig_applyUpdates);
|
||||
}
|
||||
|
||||
Class cellClass = NSClassFromString(@"IGDirectMessageCell");
|
||||
if (cellClass) {
|
||||
SEL configSel = NSSelectorFromString(@"configureWithViewModel:ringViewSpecFactory:launcherSet:");
|
||||
if (class_getInstanceMethod(cellClass, configSel))
|
||||
MSHookMessageEx(cellClass, configSel,
|
||||
(IMP)new_configureCell, (IMP *)&orig_configureCell);
|
||||
}
|
||||
|
||||
Class removeMutationClass = NSClassFromString(@"IGDirectMessageOutgoingUpdateRemoveMessagesMutationProcessor");
|
||||
if (removeMutationClass) {
|
||||
SEL execSel = NSSelectorFromString(@"executeWithResultHandler:accessoryPackage:");
|
||||
if (class_getInstanceMethod(removeMutationClass, execSel))
|
||||
MSHookMessageEx(removeMutationClass, execSel,
|
||||
(IMP)new_removeMutation_execute, (IMP *)&orig_removeMutation_execute);
|
||||
}
|
||||
|
||||
if (![SCIUtils getBoolPref:@"indicate_unsent_messages"]) {
|
||||
sciClearPreservedIds();
|
||||
}
|
||||
|
||||
return %orig(arg1);
|
||||
}
|
||||
%end
|
||||
@@ -500,6 +500,9 @@
|
||||
@interface IGDirectMessageSenderFeatureController : NSObject
|
||||
@end
|
||||
|
||||
@interface MDCoreDelta : NSObject
|
||||
@end
|
||||
|
||||
@interface IGTabBarButton : UIButton
|
||||
- (void)addHandleLongPress; // new
|
||||
@end
|
||||
|
||||
@@ -139,10 +139,31 @@
|
||||
navSections:@[@{
|
||||
@"header": @"Messages",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Keep deleted messages" subtitle:@"Saves deleted messages in chat conversations" defaultsKey:@"keep_deleted_message"],
|
||||
[SCISetting switchCellWithTitle:@"Manually mark messages as seen" subtitle:@"Adds a button to DM threads, which will mark messages as seen" defaultsKey:@"remove_lastseen"],
|
||||
[SCISetting menuCellWithTitle:@"Read receipt mode" subtitle:@"How the seen button behaves" menu:[self menus][@"seen_mode"]],
|
||||
[SCISetting switchCellWithTitle:@"Auto: mark seen on interact" subtitle:@"Locally marks messages as seen when you send a message or react" defaultsKey:@"seen_auto_on_interact"],
|
||||
[SCISetting navigationCellWithTitle:@"Keep deleted messages"
|
||||
subtitle:@"Preserve messages that others unsend"
|
||||
icon:nil
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"footer": @"Pull to refresh in the messages tab will remove preserved messages",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Keep deleted messages" subtitle:@"Preserves messages that others unsend" defaultsKey:@"keep_deleted_message"],
|
||||
[SCISetting switchCellWithTitle:@"Indicate unsent messages" subtitle:@"Shows an \"Unsent\" label on preserved messages" defaultsKey:@"indicate_unsent_messages"],
|
||||
[SCISetting switchCellWithTitle:@"Unsent message notification" subtitle:@"Shows a notification pill when a message is unsent" defaultsKey:@"unsent_message_toast"],
|
||||
]
|
||||
}]
|
||||
],
|
||||
[SCISetting navigationCellWithTitle:@"Read receipts"
|
||||
subtitle:@"Control when messages are marked as seen"
|
||||
icon:nil
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Manually mark messages as seen" subtitle:@"Adds a button to DM threads to mark messages as seen" defaultsKey:@"remove_lastseen"],
|
||||
[SCISetting menuCellWithTitle:@"Read receipt mode" subtitle:@"How the seen button behaves" menu:[self menus][@"seen_mode"]],
|
||||
[SCISetting switchCellWithTitle:@"Auto mark seen on interact" subtitle:@"Locally marks messages as seen when you send any message" defaultsKey:@"seen_auto_on_interact"],
|
||||
]
|
||||
}]
|
||||
],
|
||||
[SCISetting switchCellWithTitle:@"Disable typing status" subtitle:@"Prevents the typing indicator from being shown to others when you're typing in DMs" defaultsKey:@"disable_typing_status"],
|
||||
[SCISetting switchCellWithTitle:@"Send audio as file" subtitle:@"Adds an 'Audio File' option to the plus menu in DMs to send audio files as voice messages" defaultsKey:@"send_audio_as_file"],
|
||||
]
|
||||
|
||||
+3
-1
@@ -49,7 +49,9 @@ BOOL dmVisualMsgsViewedButtonEnabled = false;
|
||||
@"send_audio_as_file": @(YES),
|
||||
@"unlock_password_reels": @(YES),
|
||||
@"seen_mode": @"button",
|
||||
@"seen_auto_on_interact": @(YES)
|
||||
@"seen_auto_on_interact": @(YES),
|
||||
@"indicate_unsent_messages": @(NO),
|
||||
@"unsent_message_toast": @(NO)
|
||||
};
|
||||
[[NSUserDefaults standardUserDefaults] registerDefaults:sciDefaults];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user