mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-07-31 08:27:26 +02:00
feat: Per-chat and per-story blocking modes — "Block all" (exclude list) or "Block selected" (include list) with independent storage, per-entry
keep-deleted override, and adaptive UI feat: Quick list buttons in chats and stories — add/remove directly from DM threads and story viewer fix: KVO observer crash from multiple registrations in story audio toggle fix: Seen auto-bypass race condition when overlapping events (boolean → counter) fix: Confirm reel refresh not working after first pull fix: Startup class scan replaced with direct class lookup imp: All menu/button text adapts to active blocking mode imp: Mark-seen triggers at the correct point per mode imp: Migrated unexclude_inbox_button to chat_quick_list_button imp: Menu changes in settings now reload table for dynamic titles
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
}
|
||||
%end
|
||||
|
||||
static BOOL sciReelRefreshInFlight = NO;
|
||||
static BOOL sciReelRefreshBypassing = NO;
|
||||
|
||||
%hook IGSundialFeedViewController
|
||||
- (void)_refreshReelsWithParamsForNetworkRequest:(NSInteger)arg1 userDidPullToRefresh:(BOOL)arg2 {
|
||||
@@ -38,7 +38,7 @@ static BOOL sciReelRefreshInFlight = NO;
|
||||
return;
|
||||
}
|
||||
|
||||
if (![(UIViewController *)self isViewLoaded] || sciReelRefreshInFlight || ![SCIUtils getBoolPref:@"refresh_reel_confirm"]) {
|
||||
if (![(UIViewController *)self isViewLoaded] || sciReelRefreshBypassing || ![SCIUtils getBoolPref:@"refresh_reel_confirm"]) {
|
||||
%orig(arg1, arg2);
|
||||
return;
|
||||
}
|
||||
@@ -60,10 +60,10 @@ static BOOL sciReelRefreshInFlight = NO;
|
||||
__weak id weakSelf = self;
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Refresh" style:UIAlertActionStyleDefault handler:^(UIAlertAction *_) {
|
||||
sciReelRefreshInFlight = YES;
|
||||
sciReelRefreshBypassing = YES;
|
||||
SEL rSel = @selector(_refreshReelsWithParamsForNetworkRequest:userDidPullToRefresh:);
|
||||
((void(*)(id,SEL,NSInteger,BOOL))objc_msgSend)(weakSelf, rSel, arg1, arg2);
|
||||
sciReelRefreshInFlight = NO;
|
||||
sciReelRefreshBypassing = NO;
|
||||
}]];
|
||||
|
||||
UIViewController *presenter = (UIViewController *)self;
|
||||
|
||||
@@ -72,12 +72,15 @@ static id new_ctxMenuCfg(id self, SEL _cmd, id indexPath) {
|
||||
|
||||
UIContextMenuActionProvider wrapped = ^UIMenu *(NSArray<UIMenuElement *> *suggested) {
|
||||
UIMenu *base = origProvider ? origProvider(suggested) : [UIMenu menuWithChildren:suggested];
|
||||
BOOL excluded = [SCIExcludedThreads isThreadIdExcluded:tid];
|
||||
NSString *title = excluded ? @"Un-exclude chat" : @"Exclude chat";
|
||||
UIImage *img = [UIImage systemImageNamed:excluded ? @"eye.fill" : @"eye.slash"];
|
||||
BOOL inList = [SCIExcludedThreads isInList:tid];
|
||||
BOOL blockSelected = [SCIExcludedThreads isBlockSelectedMode];
|
||||
NSString *addLabel = blockSelected ? @"Add to block list" : @"Exclude chat";
|
||||
NSString *removeLabel = blockSelected ? @"Remove from block list" : @"Un-exclude chat";
|
||||
NSString *title = inList ? removeLabel : addLabel;
|
||||
UIImage *img = [UIImage systemImageNamed:inList ? @"eye.fill" : @"eye.slash"];
|
||||
UIAction *toggle = [UIAction actionWithTitle:title image:img identifier:nil
|
||||
handler:^(__kindof UIAction *_) {
|
||||
if (excluded) {
|
||||
if (inList) {
|
||||
[SCIExcludedThreads removeThreadId:tid];
|
||||
} else {
|
||||
[SCIExcludedThreads addOrUpdateEntry:entry];
|
||||
@@ -123,20 +126,9 @@ static id new_ctxMenuCfg(id self, SEL _cmd, id indexPath) {
|
||||
%end
|
||||
|
||||
%ctor {
|
||||
Class cls = NSClassFromString(@"IGDirectInboxViewController");
|
||||
if (!cls) return;
|
||||
SEL sel = NSSelectorFromString(@"networkingCoordinator_contextMenuConfigurationForThreadCellAtIndexPath:");
|
||||
unsigned int n = 0;
|
||||
Class *all = objc_copyClassList(&n);
|
||||
for (unsigned int i = 0; i < n; i++) {
|
||||
unsigned int mn = 0;
|
||||
Method *ms = class_copyMethodList(all[i], &mn);
|
||||
BOOL has = NO;
|
||||
for (unsigned int j = 0; j < mn; j++) {
|
||||
if (sel_isEqual(method_getName(ms[j]), sel)) { has = YES; break; }
|
||||
}
|
||||
if (ms) free(ms);
|
||||
if (has) {
|
||||
MSHookMessageEx(all[i], sel, (IMP)new_ctxMenuCfg, (IMP *)&orig_ctxMenuCfg);
|
||||
}
|
||||
}
|
||||
if (all) free(all);
|
||||
if (class_getInstanceMethod(cls, sel))
|
||||
MSHookMessageEx(cls, sel, (IMP)new_ctxMenuCfg, (IMP *)&orig_ctxMenuCfg);
|
||||
}
|
||||
|
||||
@@ -215,27 +215,30 @@ NSArray *sciMaybeAppendStoryExcludeMenuItem(NSArray *items) {
|
||||
NSString *username = ownerInfo[@"username"] ?: @"";
|
||||
NSString *fullName = ownerInfo[@"fullName"] ?: @"";
|
||||
// Bypass master toggle so the 3-dot fallback always shows
|
||||
BOOL excluded = NO;
|
||||
for (NSDictionary *e in [SCIExcludedStoryUsers allEntries]) {
|
||||
if ([e[@"pk"] isEqualToString:pk]) { excluded = YES; break; }
|
||||
}
|
||||
BOOL inList = [SCIExcludedStoryUsers isInList:pk];
|
||||
BOOL blockSelected = [SCIExcludedStoryUsers isBlockSelectedMode];
|
||||
|
||||
Class menuItemCls = NSClassFromString(@"IGDSMenuItem");
|
||||
if (!menuItemCls) return items;
|
||||
|
||||
NSString *title = excluded ? @"Un-exclude story seen" : @"Exclude story seen";
|
||||
NSString *addLabel = blockSelected ? @"Add to block list" : @"Exclude story seen";
|
||||
NSString *removeLabel = blockSelected ? @"Remove from block list" : @"Un-exclude story seen";
|
||||
NSString *title = inList ? removeLabel : addLabel;
|
||||
|
||||
__weak UIViewController *weakVC = sciActiveStoryViewerVC;
|
||||
void (^handler)(void) = ^{
|
||||
if (excluded) {
|
||||
if (inList) {
|
||||
[SCIExcludedStoryUsers removePK:pk];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Un-excluded"];
|
||||
[SCIUtils showToastForDuration:2.0 title:blockSelected ? @"Unblocked" : @"Un-excluded"];
|
||||
// Removing in block_selected = normal behavior → mark seen
|
||||
if (blockSelected) sciTriggerStoryMarkSeen(weakVC);
|
||||
} else {
|
||||
[SCIExcludedStoryUsers addOrUpdateEntry:@{
|
||||
@"pk": pk, @"username": username, @"fullName": fullName
|
||||
}];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Excluded"];
|
||||
sciTriggerStoryMarkSeen(weakVC);
|
||||
[SCIUtils showToastForDuration:2.0 title:blockSelected ? @"Blocked" : @"Excluded"];
|
||||
// Adding in block_all = normal behavior → mark seen
|
||||
if (!blockSelected) sciTriggerStoryMarkSeen(weakVC);
|
||||
}
|
||||
sciRefreshAllVisibleOverlays(weakVC);
|
||||
};
|
||||
|
||||
@@ -179,19 +179,34 @@ static void sciDownloadDMVisualMessage(UIViewController *dmVC) {
|
||||
|
||||
// Rebuilds the eye button (tag 1339) based on current owner + prefs. Idempotent.
|
||||
%new - (void)sciRefreshSeenButton {
|
||||
if (![SCIUtils getBoolPref:@"no_seen_receipt"]) return;
|
||||
if ([SCIExcludedThreads isActiveThreadExcluded]) return;
|
||||
BOOL seenBlockingOn = [SCIUtils getBoolPref:@"no_seen_receipt"];
|
||||
BOOL storyBlockSelected = [SCIExcludedStoryUsers isBlockSelectedMode];
|
||||
// In block_selected mode, show the eye for list management even if global toggle is off
|
||||
if (!seenBlockingOn && !storyBlockSelected) return;
|
||||
// Skip for DM visual messages inside an excluded thread
|
||||
NSString *activeTid = [SCIExcludedThreads activeThreadId];
|
||||
if (activeTid && [SCIExcludedThreads isInList:activeTid] && ![SCIExcludedThreads isBlockSelectedMode]) return;
|
||||
|
||||
NSDictionary *ownerInfo = sciOwnerInfoForView(self);
|
||||
NSString *ownerPK = ownerInfo[@"pk"] ?: @"";
|
||||
BOOL ownerExcluded = ownerInfo && [SCIExcludedStoryUsers isUserPKExcluded:ownerPK];
|
||||
BOOL hideForExcludedOwner = ownerExcluded && ![SCIUtils getBoolPref:@"story_excluded_show_unexclude_eye"];
|
||||
BOOL ownerInList = ownerPK.length && [SCIExcludedStoryUsers isInList:ownerPK];
|
||||
// block_all + in list: show remove icon (excluded user, behaves normally)
|
||||
// block_selected + in list: show normal eye (blocked user, needs mark-seen)
|
||||
// block_selected + not in list: show add icon
|
||||
BOOL showExcludeIcon = ownerInList && !storyBlockSelected;
|
||||
BOOL showAddIcon = storyBlockSelected && !ownerInList;
|
||||
BOOL listBtnPref = [SCIUtils getBoolPref:@"story_excluded_show_unexclude_eye"];
|
||||
BOOL hideForListedOwner = (showExcludeIcon || showAddIcon) && !listBtnPref;
|
||||
BOOL toggleMode = [[SCIUtils getStringPref:@"story_seen_mode"] isEqualToString:@"toggle"];
|
||||
|
||||
NSString *symName;
|
||||
UIColor *tint;
|
||||
if (ownerExcluded) {
|
||||
if (showExcludeIcon) {
|
||||
// block_all + in list: remove-from-exclude icon
|
||||
symName = @"eye.slash.fill"; tint = SCIUtils.SCIColor_Primary;
|
||||
} else if (storyBlockSelected && !ownerInList) {
|
||||
// block_selected + not in list: add-to-block icon
|
||||
symName = @"eye.slash"; tint = [UIColor whiteColor];
|
||||
} else if (toggleMode) {
|
||||
symName = sciStorySeenToggleEnabled ? @"eye.fill" : @"eye";
|
||||
tint = sciStorySeenToggleEnabled ? SCIUtils.SCIColor_Primary : [UIColor whiteColor];
|
||||
@@ -201,7 +216,7 @@ static void sciDownloadDMVisualMessage(UIViewController *dmVC) {
|
||||
|
||||
UIButton *existing = (UIButton *)[self viewWithTag:1339];
|
||||
|
||||
if (hideForExcludedOwner) {
|
||||
if (hideForListedOwner) {
|
||||
[existing removeFromSuperview];
|
||||
return;
|
||||
}
|
||||
@@ -320,18 +335,49 @@ static void sciDownloadDMVisualMessage(UIViewController *dmVC) {
|
||||
|
||||
%new - (void)sciSeenButtonTapped:(UIButton *)sender {
|
||||
NSDictionary *ownerInfo = sciOwnerInfoForView(self);
|
||||
BOOL excluded = ownerInfo && [SCIExcludedStoryUsers isUserPKExcluded:ownerInfo[@"pk"]];
|
||||
NSString *ownerPK = ownerInfo[@"pk"];
|
||||
BOOL inList = ownerPK && [SCIExcludedStoryUsers isInList:ownerPK];
|
||||
BOOL bs = [SCIExcludedStoryUsers isBlockSelectedMode];
|
||||
|
||||
// Excluded owner: tap to un-exclude
|
||||
if (excluded) {
|
||||
// Block selected + not in list: tap to ADD to block list (with confirmation)
|
||||
if (bs && !inList && ownerPK) {
|
||||
UIViewController *host = [SCIUtils nearestViewControllerForView:self];
|
||||
UIAlertController *alert = [UIAlertController
|
||||
alertControllerWithTitle:@"Un-exclude story seen?"
|
||||
message:[NSString stringWithFormat:@"@%@ will resume normal story-seen blocking.", ownerInfo[@"username"] ?: @""]
|
||||
alertControllerWithTitle:@"Add to block list?"
|
||||
message:[NSString stringWithFormat:@"Story seen receipts will be blocked for @%@.", ownerInfo[@"username"] ?: @""]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Un-exclude" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *_) {
|
||||
[SCIExcludedStoryUsers removePK:ownerInfo[@"pk"]];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Un-excluded"];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Add" style:UIAlertActionStyleDefault handler:^(UIAlertAction *_) {
|
||||
[SCIExcludedStoryUsers addOrUpdateEntry:@{
|
||||
@"pk": ownerPK,
|
||||
@"username": ownerInfo[@"username"] ?: @"",
|
||||
@"fullName": ownerInfo[@"fullName"] ?: @""
|
||||
}];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Added to block list"];
|
||||
sciRefreshAllVisibleOverlays(sciActiveStoryViewerVC);
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
[host presentViewController:alert animated:YES completion:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
// Block selected + in list: blocked story, tap = mark seen (long-press to remove)
|
||||
if (bs && inList) {
|
||||
((void(*)(id, SEL, id))objc_msgSend)(self, @selector(sciMarkSeenTapped:), sender);
|
||||
return;
|
||||
}
|
||||
|
||||
// Block all + in list: tap to remove from exclude list
|
||||
if (inList) {
|
||||
UIViewController *host = [SCIUtils nearestViewControllerForView:self];
|
||||
NSString *alertTitle = bs ? @"Remove from block list?" : @"Un-exclude story seen?";
|
||||
NSString *alertMsg = bs ? [NSString stringWithFormat:@"@%@ will no longer have seen receipts blocked.", ownerInfo[@"username"] ?: @""]
|
||||
: [NSString stringWithFormat:@"@%@ will resume normal story-seen blocking.", ownerInfo[@"username"] ?: @""];
|
||||
UIAlertController *alert = [UIAlertController
|
||||
alertControllerWithTitle:alertTitle message:alertMsg preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:bs ? @"Unblock" : @"Un-exclude" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *_) {
|
||||
[SCIExcludedStoryUsers removePK:ownerPK];
|
||||
[SCIUtils showToastForDuration:2.0 title:bs ? @"Unblocked" : @"Un-excluded"];
|
||||
if (bs) sciTriggerStoryMarkSeen(sciActiveStoryViewerVC);
|
||||
sciRefreshAllVisibleOverlays(sciActiveStoryViewerVC);
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
@@ -368,22 +414,26 @@ static void sciDownloadDMVisualMessage(UIViewController *dmVC) {
|
||||
NSString *pk = ownerInfo[@"pk"];
|
||||
NSString *username = ownerInfo[@"username"] ?: @"";
|
||||
NSString *fullName = ownerInfo[@"fullName"] ?: @"";
|
||||
BOOL excluded = pk && [SCIExcludedStoryUsers isUserPKExcluded:pk];
|
||||
BOOL inList = pk && [SCIExcludedStoryUsers isInList:pk];
|
||||
BOOL blockSelected = [SCIExcludedStoryUsers isBlockSelectedMode];
|
||||
|
||||
UIAlertController *sheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
[sheet addAction:[UIAlertAction actionWithTitle:@"Mark seen" style:UIAlertActionStyleDefault handler:^(UIAlertAction *_) {
|
||||
((void(*)(id, SEL, id))objc_msgSend)(self, @selector(sciMarkSeenTapped:), btn);
|
||||
}]];
|
||||
if (pk) {
|
||||
NSString *t = excluded ? @"Un-exclude story seen" : @"Exclude story seen";
|
||||
[sheet addAction:[UIAlertAction actionWithTitle:t style:excluded ? UIAlertActionStyleDestructive : UIAlertActionStyleDefault handler:^(UIAlertAction *_) {
|
||||
if (excluded) {
|
||||
NSString *addLabel = blockSelected ? @"Add to block list" : @"Exclude story seen";
|
||||
NSString *removeLabel = blockSelected ? @"Remove from block list" : @"Un-exclude story seen";
|
||||
NSString *t = inList ? removeLabel : addLabel;
|
||||
[sheet addAction:[UIAlertAction actionWithTitle:t style:inList ? UIAlertActionStyleDestructive : UIAlertActionStyleDefault handler:^(UIAlertAction *_) {
|
||||
if (inList) {
|
||||
[SCIExcludedStoryUsers removePK:pk];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Un-excluded"];
|
||||
[SCIUtils showToastForDuration:2.0 title:blockSelected ? @"Unblocked" : @"Un-excluded"];
|
||||
if (blockSelected) sciTriggerStoryMarkSeen(sciActiveStoryViewerVC);
|
||||
} else {
|
||||
[SCIExcludedStoryUsers addOrUpdateEntry:@{ @"pk": pk, @"username": username, @"fullName": fullName }];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Excluded"];
|
||||
sciTriggerStoryMarkSeen(sciActiveStoryViewerVC);
|
||||
[SCIUtils showToastForDuration:2.0 title:blockSelected ? @"Blocked" : @"Excluded"];
|
||||
if (!blockSelected) sciTriggerStoryMarkSeen(sciActiveStoryViewerVC);
|
||||
}
|
||||
sciRefreshAllVisibleOverlays(sciActiveStoryViewerVC);
|
||||
}]];
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
@interface SCIExcludedStoryUsers : NSObject
|
||||
|
||||
+ (BOOL)isFeatureEnabled;
|
||||
+ (BOOL)isBlockSelectedMode;
|
||||
|
||||
+ (BOOL)isUserPKExcluded:(NSString *)pk;
|
||||
+ (BOOL)isInList:(NSString *)pk;
|
||||
+ (NSDictionary *)entryForPK:(NSString *)pk;
|
||||
+ (NSArray<NSDictionary *> *)allEntries;
|
||||
+ (NSUInteger)count;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
#define SCI_STORY_EXCL_KEY @"excluded_story_users"
|
||||
#define SCI_STORY_INCL_KEY @"included_story_users"
|
||||
|
||||
@implementation SCIExcludedStoryUsers
|
||||
|
||||
@@ -9,15 +10,22 @@
|
||||
return [SCIUtils getBoolPref:@"enable_story_user_exclusions"];
|
||||
}
|
||||
|
||||
+ (BOOL)isBlockSelectedMode {
|
||||
return [[SCIUtils getStringPref:@"story_blocking_mode"] isEqualToString:@"block_selected"];
|
||||
}
|
||||
|
||||
+ (NSString *)activeKey {
|
||||
return [self isBlockSelectedMode] ? SCI_STORY_INCL_KEY : SCI_STORY_EXCL_KEY;
|
||||
}
|
||||
|
||||
+ (NSArray<NSDictionary *> *)allEntries {
|
||||
NSArray *raw = [[NSUserDefaults standardUserDefaults] arrayForKey:SCI_STORY_EXCL_KEY];
|
||||
return raw ?: @[];
|
||||
return [[NSUserDefaults standardUserDefaults] arrayForKey:[self activeKey]] ?: @[];
|
||||
}
|
||||
|
||||
+ (NSUInteger)count { return [self allEntries].count; }
|
||||
|
||||
+ (void)saveAll:(NSArray *)entries {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:entries forKey:SCI_STORY_EXCL_KEY];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:entries forKey:[self activeKey]];
|
||||
}
|
||||
|
||||
+ (NSDictionary *)entryForPK:(NSString *)pk {
|
||||
@@ -28,9 +36,14 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (BOOL)isInList:(NSString *)pk {
|
||||
return [self entryForPK:pk] != nil;
|
||||
}
|
||||
|
||||
+ (BOOL)isUserPKExcluded:(NSString *)pk {
|
||||
if (![self isFeatureEnabled]) return NO;
|
||||
return [self entryForPK:pk] != nil;
|
||||
BOOL inList = [self isInList:pk];
|
||||
return [self isBlockSelectedMode] ? !inList : inList;
|
||||
}
|
||||
|
||||
+ (void)addOrUpdateEntry:(NSDictionary *)entry {
|
||||
|
||||
@@ -14,8 +14,10 @@ typedef NS_ENUM(NSInteger, SCIKeepDeletedOverride) {
|
||||
@interface SCIExcludedThreads : NSObject
|
||||
|
||||
+ (BOOL)isFeatureEnabled;
|
||||
+ (BOOL)isBlockSelectedMode; // YES = only listed chats get blocked
|
||||
|
||||
+ (BOOL)isThreadIdExcluded:(NSString *)threadId;
|
||||
+ (BOOL)isInList:(NSString *)threadId; // raw list check, ignores mode
|
||||
+ (BOOL)shouldKeepDeletedBeBlockedForThreadId:(NSString *)threadId;
|
||||
+ (NSDictionary *)entryForThreadId:(NSString *)threadId;
|
||||
+ (NSArray<NSDictionary *> *)allEntries;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
#define SCI_EXCL_KEY @"excluded_threads"
|
||||
#define SCI_INCL_KEY @"included_threads"
|
||||
|
||||
@implementation SCIExcludedThreads
|
||||
|
||||
@@ -11,17 +12,22 @@ static NSString *sciActiveTid = nil;
|
||||
return [SCIUtils getBoolPref:@"enable_chat_exclusions"];
|
||||
}
|
||||
|
||||
+ (NSArray<NSDictionary *> *)allEntries {
|
||||
NSArray *raw = [[NSUserDefaults standardUserDefaults] arrayForKey:SCI_EXCL_KEY];
|
||||
return raw ?: @[];
|
||||
+ (BOOL)isBlockSelectedMode {
|
||||
return [[SCIUtils getStringPref:@"chat_blocking_mode"] isEqualToString:@"block_selected"];
|
||||
}
|
||||
|
||||
+ (NSUInteger)count {
|
||||
return [self allEntries].count;
|
||||
+ (NSString *)activeKey {
|
||||
return [self isBlockSelectedMode] ? SCI_INCL_KEY : SCI_EXCL_KEY;
|
||||
}
|
||||
|
||||
+ (NSArray<NSDictionary *> *)allEntries {
|
||||
return [[NSUserDefaults standardUserDefaults] arrayForKey:[self activeKey]] ?: @[];
|
||||
}
|
||||
|
||||
+ (NSUInteger)count { return [self allEntries].count; }
|
||||
|
||||
+ (void)saveAll:(NSArray *)entries {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:entries forKey:SCI_EXCL_KEY];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:entries forKey:[self activeKey]];
|
||||
}
|
||||
|
||||
+ (NSDictionary *)entryForThreadId:(NSString *)threadId {
|
||||
@@ -32,14 +38,32 @@ static NSString *sciActiveTid = nil;
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (BOOL)isInList:(NSString *)threadId {
|
||||
return [self entryForThreadId:threadId] != nil;
|
||||
}
|
||||
|
||||
+ (BOOL)isThreadIdExcluded:(NSString *)threadId {
|
||||
if (![self isFeatureEnabled]) return NO;
|
||||
return [self entryForThreadId:threadId] != nil;
|
||||
BOOL inList = [self isInList:threadId];
|
||||
return [self isBlockSelectedMode] ? !inList : inList;
|
||||
}
|
||||
|
||||
+ (BOOL)shouldKeepDeletedBeBlockedForThreadId:(NSString *)threadId {
|
||||
if (![self isFeatureEnabled]) return NO;
|
||||
NSDictionary *e = [self entryForThreadId:threadId];
|
||||
|
||||
if ([self isBlockSelectedMode]) {
|
||||
// block_selected: listed chats are blocked
|
||||
// NOT in list → normal chat → block keep-deleted if default pref is on
|
||||
// IN list → blocked chat → keep-deleted should work (not blocked) unless overridden
|
||||
if (!e) return [SCIUtils getBoolPref:@"exclusions_default_keep_deleted"];
|
||||
SCIKeepDeletedOverride mode = [e[@"keepDeletedOverride"] integerValue];
|
||||
if (mode == SCIKeepDeletedOverrideExcluded) return YES;
|
||||
if (mode == SCIKeepDeletedOverrideIncluded) return NO;
|
||||
return NO; // default: keep-deleted works in blocked chats
|
||||
}
|
||||
|
||||
// block_all: listed chats are excluded (behave normally)
|
||||
if (!e) return NO;
|
||||
SCIKeepDeletedOverride mode = [e[@"keepDeletedOverride"] integerValue];
|
||||
if (mode == SCIKeepDeletedOverrideExcluded) return YES;
|
||||
@@ -57,7 +81,6 @@ static NSString *sciActiveTid = nil;
|
||||
}
|
||||
NSMutableDictionary *merged = [entry mutableCopy];
|
||||
if (existingIdx >= 0) {
|
||||
// Preserve existing addedAt + override
|
||||
NSDictionary *old = all[existingIdx];
|
||||
if (old[@"addedAt"]) merged[@"addedAt"] = old[@"addedAt"];
|
||||
if (old[@"keepDeletedOverride"]) merged[@"keepDeletedOverride"] = old[@"keepDeletedOverride"];
|
||||
|
||||
@@ -18,7 +18,7 @@ static NSString *sciThreadIdForVC(id vc) {
|
||||
// - Enables unlimited views of DM visual messages
|
||||
|
||||
BOOL dmSeenToggleEnabled = NO;
|
||||
static BOOL sciSeenAutoBypass = NO;
|
||||
static NSInteger sciSeenAutoBypassCount = 0;
|
||||
__weak IGDirectThreadViewController *sciActiveThreadVC = nil;
|
||||
|
||||
static BOOL sciIsSeenToggleMode() {
|
||||
@@ -36,10 +36,10 @@ BOOL sciAutoTypingEnabled() {
|
||||
}
|
||||
|
||||
void sciDoAutoSeen(IGDirectThreadViewController *threadVC) {
|
||||
sciSeenAutoBypass = YES;
|
||||
sciSeenAutoBypassCount++;
|
||||
[threadVC markLastMessageAsSeen];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
sciSeenAutoBypass = NO;
|
||||
sciSeenAutoBypassCount--;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,9 +79,13 @@ static void sciRefreshNavBarItems(UIView *anchor) {
|
||||
[(id)anchor performSelector:@selector(setRightBarButtonItems:) withObject:cur];
|
||||
}
|
||||
|
||||
static NSDictionary *sciEntryFromThreadVC(UIViewController *vc);
|
||||
|
||||
// Long-press menu shared by the seen button and the un-exclude button.
|
||||
static UIMenu *sciBuildThreadActionsMenu(UIView *anchor, NSString *threadId, UIWindow *window) {
|
||||
BOOL inList = threadId && [SCIExcludedThreads isInList:threadId];
|
||||
BOOL excluded = threadId && [SCIExcludedThreads isThreadIdExcluded:threadId];
|
||||
BOOL blockSelected = [SCIExcludedThreads isBlockSelectedMode];
|
||||
BOOL seenFeatureOn = [SCIUtils getBoolPref:@"remove_lastseen"];
|
||||
|
||||
NSMutableArray<UIMenuElement *> *items = [NSMutableArray array];
|
||||
@@ -117,25 +121,35 @@ static UIMenu *sciBuildThreadActionsMenu(UIView *anchor, NSString *threadId, UIW
|
||||
[items addObject:seenAction];
|
||||
}
|
||||
|
||||
NSString *toggleTitle = excluded ? @"Un-exclude chat" : @"Exclude chat";
|
||||
UIImage *toggleImg = [UIImage systemImageNamed:excluded ? @"eye.fill" : @"eye.slash"];
|
||||
NSString *addLabel = blockSelected ? @"Add to block list" : @"Exclude chat";
|
||||
NSString *removeLabel = blockSelected ? @"Remove from block list" : @"Un-exclude chat";
|
||||
NSString *toggleTitle = inList ? removeLabel : addLabel;
|
||||
UIImage *toggleImg = [UIImage systemImageNamed:inList ? @"eye.fill" : @"eye.slash"];
|
||||
__weak UIView *weakAnchor = anchor;
|
||||
UIAction *toggle = [UIAction actionWithTitle:toggleTitle image:toggleImg identifier:nil
|
||||
handler:^(__kindof UIAction *_) {
|
||||
if (!threadId) return;
|
||||
if (excluded) {
|
||||
if (inList) {
|
||||
[SCIExcludedThreads removeThreadId:threadId];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Un-excluded"];
|
||||
[SCIUtils showToastForDuration:2.0 title:blockSelected ? @"Unblocked" : @"Un-excluded"];
|
||||
// In block_selected, removing = normal behavior → mark seen
|
||||
if (blockSelected) {
|
||||
UIViewController *nearestVC = [SCIUtils nearestViewControllerForView:weakAnchor];
|
||||
if ([nearestVC isKindOfClass:%c(IGDirectThreadViewController)])
|
||||
[(IGDirectThreadViewController *)nearestVC markLastMessageAsSeen];
|
||||
}
|
||||
} else {
|
||||
[SCIExcludedThreads addOrUpdateEntry:@{ @"threadId": threadId,
|
||||
@"threadName": @"",
|
||||
@"isGroup": @NO,
|
||||
@"users": @[] }];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Excluded"];
|
||||
// Immediately mark seen since exclusion means normal behavior.
|
||||
UIViewController *nearestVC = [SCIUtils nearestViewControllerForView:weakAnchor];
|
||||
if ([nearestVC isKindOfClass:%c(IGDirectThreadViewController)])
|
||||
[(IGDirectThreadViewController *)nearestVC markLastMessageAsSeen];
|
||||
UIViewController *anchorVC = [SCIUtils nearestViewControllerForView:anchor];
|
||||
NSDictionary *entry = sciEntryFromThreadVC(anchorVC);
|
||||
if (!entry) entry = @{ @"threadId": threadId, @"threadName": @"", @"isGroup": @NO, @"users": @[] };
|
||||
[SCIExcludedThreads addOrUpdateEntry:entry];
|
||||
[SCIUtils showToastForDuration:2.0 title:blockSelected ? @"Blocked" : @"Excluded"];
|
||||
// In block_all, excluding = normal behavior → mark seen
|
||||
if (!blockSelected) {
|
||||
UIViewController *nearestVC = [SCIUtils nearestViewControllerForView:weakAnchor];
|
||||
if ([nearestVC isKindOfClass:%c(IGDirectThreadViewController)])
|
||||
[(IGDirectThreadViewController *)nearestVC markLastMessageAsSeen];
|
||||
}
|
||||
}
|
||||
sciRefreshNavBarItems(weakAnchor);
|
||||
}];
|
||||
@@ -159,21 +173,74 @@ static UIMenu *sciBuildThreadActionsMenu(UIView *anchor, NSString *threadId, UIW
|
||||
return [UIMenu menuWithTitle:@"" children:items];
|
||||
}
|
||||
|
||||
// Extract thread info from an IGDirectThreadViewController
|
||||
static NSDictionary *sciEntryFromThreadVC(UIViewController *vc) {
|
||||
if (!vc) return nil;
|
||||
NSString *tid = sciThreadIdForVC(vc);
|
||||
if (!tid) return nil;
|
||||
NSString *name = @"";
|
||||
NSMutableArray *users = [NSMutableArray array];
|
||||
@try {
|
||||
// Try to get thread title from navigation item
|
||||
name = vc.navigationItem.title ?: @"";
|
||||
// Try to get the thread object for user info
|
||||
id thread = [vc valueForKey:@"thread"];
|
||||
if (thread) {
|
||||
id threadUsers = [thread valueForKey:@"users"];
|
||||
if ([threadUsers isKindOfClass:[NSArray class]]) {
|
||||
for (id u in (NSArray *)threadUsers) {
|
||||
NSMutableDictionary *d = [NSMutableDictionary dictionary];
|
||||
@try {
|
||||
id pk = [u valueForKey:@"pk"];
|
||||
id un = [u valueForKey:@"username"];
|
||||
id fn = [u valueForKey:@"fullName"];
|
||||
if (pk) d[@"pk"] = [NSString stringWithFormat:@"%@", pk];
|
||||
if (un) d[@"username"] = [NSString stringWithFormat:@"%@", un];
|
||||
if (fn) d[@"fullName"] = [NSString stringWithFormat:@"%@", fn];
|
||||
} @catch (__unused id e) {}
|
||||
if (d.count) [users addObject:d];
|
||||
}
|
||||
}
|
||||
}
|
||||
} @catch (__unused id e) {}
|
||||
return @{ @"threadId": tid, @"threadName": name, @"isGroup": @NO, @"users": users };
|
||||
}
|
||||
|
||||
%hook IGTallNavigationBarView
|
||||
|
||||
%new - (void)sciAddToListHandler:(UIBarButtonItem *)sender {
|
||||
UIViewController *nearestVC = [SCIUtils nearestViewControllerForView:self];
|
||||
NSDictionary *entry = sciEntryFromThreadVC(nearestVC);
|
||||
if (!entry) return;
|
||||
UIAlertController *alert = [UIAlertController
|
||||
alertControllerWithTitle:@"Add to block list?"
|
||||
message:@"Read receipts will be blocked for this chat."
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Add" style:UIAlertActionStyleDefault handler:^(UIAlertAction *_) {
|
||||
[SCIExcludedThreads addOrUpdateEntry:entry];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Added to block list"];
|
||||
sciRefreshNavBarItems(weakSelf);
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
[nearestVC presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
%new - (void)sciUnexcludeButtonHandler:(UIBarButtonItem *)sender {
|
||||
UIViewController *nearestVC = [SCIUtils nearestViewControllerForView:self];
|
||||
NSString *tid = sciThreadIdForVC(nearestVC);
|
||||
if (!tid) return;
|
||||
|
||||
BOOL bs = [SCIExcludedThreads isBlockSelectedMode];
|
||||
NSString *alertTitle = bs ? @"Remove from block list?" : @"Un-exclude chat?";
|
||||
NSString *alertMsg = bs ? @"Read receipts will no longer be blocked for this chat."
|
||||
: @"This chat will resume normal read-receipt behavior.";
|
||||
UIAlertController *alert = [UIAlertController
|
||||
alertControllerWithTitle:@"Remove from exclusion?"
|
||||
message:@"This chat will resume normal read-receipt behavior."
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
alertControllerWithTitle:alertTitle message:alertMsg preferredStyle:UIAlertControllerStyleAlert];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Remove" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *_) {
|
||||
[SCIExcludedThreads removeThreadId:tid];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Removed from exclusion"];
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Removed"];
|
||||
sciRefreshNavBarItems(weakSelf);
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
@@ -198,6 +265,7 @@ static UIMenu *sciBuildThreadActionsMenu(UIView *anchor, NSString *threadId, UIW
|
||||
UIViewController *navNearestVC = [SCIUtils nearestViewControllerForView:self];
|
||||
NSString *navThreadId = sciThreadIdForVC(navNearestVC);
|
||||
BOOL navExcluded = navThreadId && [SCIExcludedThreads isThreadIdExcluded:navThreadId];
|
||||
BOOL navInList = navThreadId && [SCIExcludedThreads isInList:navThreadId];
|
||||
|
||||
if ([SCIUtils getBoolPref:@"remove_lastseen"] && !navExcluded) {
|
||||
UIBarButtonItem *seenButton = [[UIBarButtonItem alloc] initWithImage:[UIImage systemImageNamed:@"eye"] style:UIBarButtonItemStylePlain target:self action:@selector(seenButtonHandler:)];
|
||||
@@ -208,18 +276,26 @@ static UIMenu *sciBuildThreadActionsMenu(UIView *anchor, NSString *threadId, UIW
|
||||
[new_items addObject:seenButton];
|
||||
}
|
||||
|
||||
// Excluded chats hide the seen button — surface an un-exclude affordance instead.
|
||||
if ([SCIUtils getBoolPref:@"remove_lastseen"] && navExcluded &&
|
||||
[SCIUtils getBoolPref:@"unexclude_inbox_button"]) {
|
||||
UIBarButtonItem *unexBtn = [[UIBarButtonItem alloc]
|
||||
initWithImage:[UIImage systemImageNamed:@"eye.slash.fill"]
|
||||
// In block_all: show remove button for listed (excluded) chats
|
||||
// In block_selected: show remove button for listed chats, or add button for non-listed chats
|
||||
BOOL blockSelected = [SCIExcludedThreads isBlockSelectedMode];
|
||||
BOOL showListButton = [SCIUtils getBoolPref:@"remove_lastseen"] && [SCIUtils getBoolPref:@"chat_quick_list_button"];
|
||||
// block_all + in list: show remove button (no seen button shown for excluded chats)
|
||||
// block_selected + NOT in list: show add-to-list button
|
||||
// block_selected + in list: DON'T show (seen button already visible with long-press menu)
|
||||
BOOL showRemoveBtn = !blockSelected && navInList && navExcluded;
|
||||
BOOL showAddBtn = blockSelected && !navInList;
|
||||
if (showListButton && (showRemoveBtn || showAddBtn)) {
|
||||
SEL action = showRemoveBtn ? @selector(sciUnexcludeButtonHandler:) : @selector(sciAddToListHandler:);
|
||||
UIBarButtonItem *listBtn = [[UIBarButtonItem alloc]
|
||||
initWithImage:[UIImage systemImageNamed:showRemoveBtn ? @"eye.slash.fill" : @"eye.slash"]
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self
|
||||
action:@selector(sciUnexcludeButtonHandler:)];
|
||||
unexBtn.accessibilityIdentifier = @"sci-unex-btn";
|
||||
unexBtn.tintColor = SCIUtils.SCIColor_Primary;
|
||||
unexBtn.menu = sciBuildThreadActionsMenu(self, navThreadId, self.window);
|
||||
[new_items addObject:unexBtn];
|
||||
action:action];
|
||||
listBtn.accessibilityIdentifier = @"sci-unex-btn";
|
||||
listBtn.tintColor = showRemoveBtn ? SCIUtils.SCIColor_Primary : UIColor.labelColor;
|
||||
listBtn.menu = sciBuildThreadActionsMenu(self, navThreadId, self.window);
|
||||
[new_items addObject:listBtn];
|
||||
}
|
||||
|
||||
if ([SCIUtils getBoolPref:@"unlimited_replay"] && !navExcluded) {
|
||||
@@ -277,7 +353,7 @@ static UIMenu *sciBuildThreadActionsMenu(UIView *anchor, NSString *threadId, UIW
|
||||
if ([SCIUtils getBoolPref:@"remove_lastseen"]) {
|
||||
if ([SCIExcludedThreads isActiveThreadExcluded]) return %orig; // excluded → behave normally
|
||||
if (sciIsSeenToggleMode() && dmSeenToggleEnabled) return %orig;
|
||||
if (sciSeenAutoBypass) return %orig;
|
||||
if (sciSeenAutoBypassCount > 0) return %orig;
|
||||
return false;
|
||||
}
|
||||
return %orig;
|
||||
|
||||
@@ -55,19 +55,25 @@ BOOL sciIsStoryAudioEnabled(void) {
|
||||
return sciIGAudioEnabled();
|
||||
}
|
||||
|
||||
static BOOL sciKVORegistered = NO;
|
||||
|
||||
void sciInitStoryAudioState(void) {
|
||||
if (sciKVORegistered) return;
|
||||
if (!sciVolumeObserver) sciVolumeObserver = [_SciVolumeObserver new];
|
||||
@try {
|
||||
[[AVAudioSession sharedInstance] addObserver:sciVolumeObserver
|
||||
forKeyPath:@"outputVolume"
|
||||
options:NSKeyValueObservingOptionNew
|
||||
context:NULL];
|
||||
sciKVORegistered = YES;
|
||||
} @catch (__unused id e) {}
|
||||
}
|
||||
|
||||
void sciResetStoryAudioState(void) {
|
||||
if (!sciKVORegistered) return;
|
||||
@try {
|
||||
[[AVAudioSession sharedInstance] removeObserver:sciVolumeObserver forKeyPath:@"outputVolume"];
|
||||
sciKVORegistered = NO;
|
||||
} @catch (__unused id e) {}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user