From 06b262671439ddd806aa0ea7fe90e0fcc4a3d027 Mon Sep 17 00:00:00 2001 From: faroukbmiled Date: Fri, 10 Apr 2026 13:41:58 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Per-chat=20and=20per-story=20blocking?= =?UTF-8?q?=20modes=20=E2=80=94=20"Block=20all"=20(exclude=20list)=20or=20?= =?UTF-8?q?"Block=20selected"=20(include=20list)=20with=20independent=20st?= =?UTF-8?q?orage,=20per-entry=20keep-deleted=20override,=20and=20adaptive?= =?UTF-8?q?=20UI=20feat:=20Quick=20list=20buttons=20in=20chats=20and=20sto?= =?UTF-8?q?ries=20=E2=80=94=20add/remove=20directly=20from=20DM=20threads?= =?UTF-8?q?=20and=20story=20viewer=20fix:=20KVO=20observer=20crash=20from?= =?UTF-8?q?=20multiple=20registrations=20in=20story=20audio=20toggle=20fix?= =?UTF-8?q?:=20Seen=20auto-bypass=20race=20condition=20when=20overlapping?= =?UTF-8?q?=20events=20(boolean=20=E2=86=92=20counter)=20fix:=20Confirm=20?= =?UTF-8?q?reel=20refresh=20not=20working=20after=20first=20pull=20fix:=20?= =?UTF-8?q?Startup=20class=20scan=20replaced=20with=20direct=20class=20loo?= =?UTF-8?q?kup=20imp:=20All=20menu/button=20text=20adapts=20to=20active=20?= =?UTF-8?q?blocking=20mode=20imp:=20Mark-seen=20triggers=20at=20the=20corr?= =?UTF-8?q?ect=20point=20per=20mode=20imp:=20Migrated=20unexclude=5Finbox?= =?UTF-8?q?=5Fbutton=20to=20chat=5Fquick=5Flist=5Fbutton=20imp:=20Menu=20c?= =?UTF-8?q?hanges=20in=20settings=20now=20reload=20table=20for=20dynamic?= =?UTF-8?q?=20titles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- src/Features/Reels/ReelsPlayback.xm | 8 +- .../StoriesAndMessages/ExcludeFromSeen.x | 30 ++-- .../StoriesAndMessages/ExcludeFromStorySeen.x | 21 +-- .../StoriesAndMessages/OverlayButtons.xm | 92 +++++++++--- .../SCIExcludedStoryUsers.h | 2 + .../SCIExcludedStoryUsers.m | 21 ++- .../StoriesAndMessages/SCIExcludedThreads.h | 2 + .../StoriesAndMessages/SCIExcludedThreads.m | 39 ++++- src/Features/StoriesAndMessages/SeenButtons.x | 138 ++++++++++++++---- .../StoriesAndMessages/StoryAudioToggle.xm | 6 + src/Settings/SCIExcludedChatsViewController.m | 4 +- .../SCIExcludedStoryUsersViewController.m | 4 +- src/Settings/SCISettingsBackup.m | 2 + src/Settings/SCISettingsViewController.m | 3 +- src/Settings/TweakSettings.m | 57 ++++++-- src/Tweak.x | 4 +- 17 files changed, 325 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 558bb32..14a2323 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ A feature-rich iOS tweak for Instagram, forked from [SCInsta](https://github.com - Mark seen on story like **\*** - Advance to next story when marking as seen — tapping the eye button auto-skips to the next story **\*** - Advance on story like — liking a story auto-skips to the next one **\*** -- Per-chat read-receipt exclusions — long-press any DM chat to exclude it from all read-receipt features. Excluded chats behave like a vanilla install (manual seen button, auto seen on send/typing, visual message blocking, view-once override are all skipped). Settings page lists excluded chats with search, sort, swipe-to-remove, and a per-entry override to also bypass keep-deleted-messages **\*** +- Per-chat read-receipt list with blocking mode — "Block all" (exclude list) or "Block selected only" (include list). Long-press any DM chat to add/remove. Settings page with search, sort, multi-select, and per-entry keep-deleted override **\*** - Send audio as file — send audio files as voice messages from the DM plus menu **\*** - Download voice messages — adds a Download option to the long-press menu on voice messages, saves as M4A via share sheet **\*** - Disable typing status @@ -109,7 +109,7 @@ A feature-rich iOS tweak for Instagram, forked from [SCInsta](https://github.com - Keep stories visually unseen — keeps the colorful ring in the tray after viewing **\*** - Manual mark story as seen — button on story overlay to selectively mark stories as seen (button or toggle mode) **\*** - Long-press the story seen button for quick actions **\*** -- Per-user story seen-receipt exclusions — exclude specific users so their stories behave normally. Manage via 3-dot menu, eye button long-press, or settings list **\*** +- Per-user story seen-receipt list with blocking mode — "Block all" (exclude list) or "Block selected only" (include list). Manage via 3-dot menu, eye button long-press, or settings list **\*** - Story audio mute/unmute toggle — button on the story overlay and 3-dot menu to toggle audio **\*** - Stop story auto-advance — stories won't auto-skip when the timer ends **\*** - Story download button — download directly from the story overlay **\*** diff --git a/src/Features/Reels/ReelsPlayback.xm b/src/Features/Reels/ReelsPlayback.xm index 9e33bfc..2759bb5 100644 --- a/src/Features/Reels/ReelsPlayback.xm +++ b/src/Features/Reels/ReelsPlayback.xm @@ -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; diff --git a/src/Features/StoriesAndMessages/ExcludeFromSeen.x b/src/Features/StoriesAndMessages/ExcludeFromSeen.x index 301590e..cd52370 100644 --- a/src/Features/StoriesAndMessages/ExcludeFromSeen.x +++ b/src/Features/StoriesAndMessages/ExcludeFromSeen.x @@ -72,12 +72,15 @@ static id new_ctxMenuCfg(id self, SEL _cmd, id indexPath) { UIContextMenuActionProvider wrapped = ^UIMenu *(NSArray *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); } diff --git a/src/Features/StoriesAndMessages/ExcludeFromStorySeen.x b/src/Features/StoriesAndMessages/ExcludeFromStorySeen.x index 535827c..c507c11 100644 --- a/src/Features/StoriesAndMessages/ExcludeFromStorySeen.x +++ b/src/Features/StoriesAndMessages/ExcludeFromStorySeen.x @@ -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); }; diff --git a/src/Features/StoriesAndMessages/OverlayButtons.xm b/src/Features/StoriesAndMessages/OverlayButtons.xm index 14f61a8..470c148 100644 --- a/src/Features/StoriesAndMessages/OverlayButtons.xm +++ b/src/Features/StoriesAndMessages/OverlayButtons.xm @@ -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); }]]; diff --git a/src/Features/StoriesAndMessages/SCIExcludedStoryUsers.h b/src/Features/StoriesAndMessages/SCIExcludedStoryUsers.h index 22b953b..bc53948 100644 --- a/src/Features/StoriesAndMessages/SCIExcludedStoryUsers.h +++ b/src/Features/StoriesAndMessages/SCIExcludedStoryUsers.h @@ -6,8 +6,10 @@ @interface SCIExcludedStoryUsers : NSObject + (BOOL)isFeatureEnabled; ++ (BOOL)isBlockSelectedMode; + (BOOL)isUserPKExcluded:(NSString *)pk; ++ (BOOL)isInList:(NSString *)pk; + (NSDictionary *)entryForPK:(NSString *)pk; + (NSArray *)allEntries; + (NSUInteger)count; diff --git a/src/Features/StoriesAndMessages/SCIExcludedStoryUsers.m b/src/Features/StoriesAndMessages/SCIExcludedStoryUsers.m index 70d8d63..08c4138 100644 --- a/src/Features/StoriesAndMessages/SCIExcludedStoryUsers.m +++ b/src/Features/StoriesAndMessages/SCIExcludedStoryUsers.m @@ -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 *)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 { diff --git a/src/Features/StoriesAndMessages/SCIExcludedThreads.h b/src/Features/StoriesAndMessages/SCIExcludedThreads.h index 85d1642..a071a18 100644 --- a/src/Features/StoriesAndMessages/SCIExcludedThreads.h +++ b/src/Features/StoriesAndMessages/SCIExcludedThreads.h @@ -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 *)allEntries; diff --git a/src/Features/StoriesAndMessages/SCIExcludedThreads.m b/src/Features/StoriesAndMessages/SCIExcludedThreads.m index b85377a..861a5c3 100644 --- a/src/Features/StoriesAndMessages/SCIExcludedThreads.m +++ b/src/Features/StoriesAndMessages/SCIExcludedThreads.m @@ -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 *)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 *)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"]; diff --git a/src/Features/StoriesAndMessages/SeenButtons.x b/src/Features/StoriesAndMessages/SeenButtons.x index 48befc0..f477723 100644 --- a/src/Features/StoriesAndMessages/SeenButtons.x +++ b/src/Features/StoriesAndMessages/SeenButtons.x @@ -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 *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; diff --git a/src/Features/StoriesAndMessages/StoryAudioToggle.xm b/src/Features/StoriesAndMessages/StoryAudioToggle.xm index 16c26ea..1b7f68e 100644 --- a/src/Features/StoriesAndMessages/StoryAudioToggle.xm +++ b/src/Features/StoriesAndMessages/StoryAudioToggle.xm @@ -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) {} } diff --git a/src/Settings/SCIExcludedChatsViewController.m b/src/Settings/SCIExcludedChatsViewController.m index 1c44c5e..a33d619 100644 --- a/src/Settings/SCIExcludedChatsViewController.m +++ b/src/Settings/SCIExcludedChatsViewController.m @@ -154,7 +154,9 @@ }]; } self.filtered = all; - self.title = [NSString stringWithFormat:@"Chats (%lu)", (unsigned long)self.filtered.count]; + BOOL bs = [SCIExcludedThreads isBlockSelectedMode]; + NSString *label = bs ? @"Included chats" : @"Excluded chats"; + self.title = [NSString stringWithFormat:@"%@ (%lu)", label, (unsigned long)self.filtered.count]; [self.tableView reloadData]; } diff --git a/src/Settings/SCIExcludedStoryUsersViewController.m b/src/Settings/SCIExcludedStoryUsersViewController.m index 17f9a36..14fdd3f 100644 --- a/src/Settings/SCIExcludedStoryUsersViewController.m +++ b/src/Settings/SCIExcludedStoryUsersViewController.m @@ -122,7 +122,9 @@ }]; } self.filtered = all; - self.title = [NSString stringWithFormat:@"Story users (%lu)", (unsigned long)self.filtered.count]; + BOOL bs = [SCIExcludedStoryUsers isBlockSelectedMode]; + NSString *label = bs ? @"Included users" : @"Excluded users"; + self.title = [NSString stringWithFormat:@"%@ (%lu)", label, (unsigned long)self.filtered.count]; [self.tableView reloadData]; } diff --git a/src/Settings/SCISettingsBackup.m b/src/Settings/SCISettingsBackup.m index 6c5e7fc..8c1153e 100644 --- a/src/Settings/SCISettingsBackup.m +++ b/src/Settings/SCISettingsBackup.m @@ -404,7 +404,9 @@ typedef NS_ENUM(NSInteger, SCIBackupPreviewRowKind) { + (NSArray *)extraDataKeys { return @[ @"excluded_threads", + @"included_threads", @"excluded_story_users", + @"included_story_users", @"embed_custom_domains", ]; } diff --git a/src/Settings/SCISettingsViewController.m b/src/Settings/SCISettingsViewController.m index 04f57c9..8e11f34 100644 --- a/src/Settings/SCISettingsViewController.m +++ b/src/Settings/SCISettingsViewController.m @@ -386,7 +386,8 @@ static char rowStaticRef[] = "row"; NSLog(@"Menu changed: %@", command.propertyList[@"value"]); [self reloadCellForView:command.sender animated:YES]; - + [self.tableView reloadData]; + if (properties[@"requiresRestart"]) { [SCIUtils showRestartConfirmation]; } diff --git a/src/Settings/TweakSettings.m b/src/Settings/TweakSettings.m index c71d692..3e7b31a 100644 --- a/src/Settings/TweakSettings.m +++ b/src/Settings/TweakSettings.m @@ -209,11 +209,12 @@ ] }, @{ - @"header": @"Excluded users", - @"footer": @"Excluded users' stories behave normally — your view shows up in their viewer list. Add via the long-press menu on the eye button while viewing a story, or via the 3-dot menu on the story header.", + @"header": @"Story user list", + @"footer": @"Block all: all stories blocked — listed users are exceptions.\nBlock selected: only listed users are blocked — everything else is normal.\nBoth lists are saved independently.", @"rows": @[ - [SCISetting switchCellWithTitle:@"Enable story user exclusions" subtitle:@"Master toggle. When off, exclusions are ignored" defaultsKey:@"enable_story_user_exclusions"], - [SCISetting switchCellWithTitle:@"Show un-exclude eye on excluded users" subtitle:@"When viewing an excluded user's story, the eye button appears so you can un-exclude with one tap. Off = use the 3-dot menu only" defaultsKey:@"story_excluded_show_unexclude_eye"], + [SCISetting switchCellWithTitle:@"Enable story user list" subtitle:@"Master toggle. When off, the list is ignored" defaultsKey:@"enable_story_user_exclusions"], + [SCISetting menuCellWithTitle:@"Blocking mode" subtitle:@"Which stories get seen-receipt blocking" menu:[self menus][@"story_blocking_mode"]], + [SCISetting switchCellWithTitle:@"Quick list button in stories" subtitle:@"Shows an eye button on stories to add/remove users from the list. Off = use the 3-dot menu or long-press only" defaultsKey:@"story_excluded_show_unexclude_eye"], ({ SCISetting *s = [SCISetting buttonCellWithTitle:@"Manage list" subtitle:@"Search, sort, swipe to remove" @@ -279,8 +280,7 @@ [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:@"Auto mark seen on typing" subtitle:@"Marks messages as seen the moment you start typing in a DM (works even when typing status is hidden)" defaultsKey:@"seen_auto_on_typing"], - [SCISetting switchCellWithTitle:@"Un-exclude button in excluded chats" subtitle:@"Show a small eye button in excluded chats to remove them from the exclusion list (with confirmation). Long-press it for more options." defaultsKey:@"unexclude_inbox_button"], - ] + ] }] ], [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"], @@ -288,11 +288,22 @@ ] }, @{ - @"header": @"Excluded chats", - @"footer": @"Excluded chats and groups behave normally — read-receipt blocking, manual seen button, auto-seen on send/typing are all skipped for them. Long-press a chat in the inbox to add or remove it.", + @"header": @"Chat list", + @"footer": @"Block all: all chats blocked — listed chats are exceptions.\nBlock selected: only listed chats are blocked — everything else is normal.\nBoth lists are saved independently. Long-press a chat in the inbox to add or remove.", @"rows": @[ - [SCISetting switchCellWithTitle:@"Enable chat exclusions" subtitle:@"Master toggle. When off, the inbox menu item disappears and exclusions are ignored" defaultsKey:@"enable_chat_exclusions"], - [SCISetting switchCellWithTitle:@"Default: also exclude keep-deleted" subtitle:@"Excluded chats also bypass keep-deleted-messages by default. Each chat can override this in the list" defaultsKey:@"exclusions_default_keep_deleted"], + [SCISetting switchCellWithTitle:@"Enable chat list" subtitle:@"Master toggle. When off, the list is ignored" defaultsKey:@"enable_chat_exclusions"], + [SCISetting menuCellWithTitle:@"Blocking mode" subtitle:@"Which chats get read-receipt blocking" menu:[self menus][@"chat_blocking_mode"]], + ({ + SCISetting *s = [SCISetting switchCellWithTitle:@"" subtitle:@"" defaultsKey:@"exclusions_default_keep_deleted"]; + s.dynamicTitle = ^{ + BOOL bs = [[SCIUtils getStringPref:@"chat_blocking_mode"] isEqualToString:@"block_selected"]; + return bs ? @"Block keep-deleted for unlisted chats" + : @"Block keep-deleted for excluded chats"; + }; + s.subtitle = @"Each chat can override this in the list"; + s; +}), + [SCISetting switchCellWithTitle:@"Quick list button in chats" subtitle:@"Shows a button in DM threads to add/remove chats from the list. Long-press for more options" defaultsKey:@"chat_quick_list_button"], ({ SCISetting *s = [SCISetting buttonCellWithTitle:@"Manage list" subtitle:@"Search, sort, swipe to remove or toggle keep-deleted" @@ -523,6 +534,32 @@ + (NSDictionary *)menus { return @{ + @"chat_blocking_mode": [UIMenu menuWithChildren:@[ + [UICommand commandWithTitle:@"Block all" + image:nil + action:@selector(menuChanged:) + propertyList:@{ @"defaultsKey": @"chat_blocking_mode", @"value": @"block_all" } + ], + [UICommand commandWithTitle:@"Block selected" + image:nil + action:@selector(menuChanged:) + propertyList:@{ @"defaultsKey": @"chat_blocking_mode", @"value": @"block_selected" } + ] + ]], + + @"story_blocking_mode": [UIMenu menuWithChildren:@[ + [UICommand commandWithTitle:@"Block all" + image:nil + action:@selector(menuChanged:) + propertyList:@{ @"defaultsKey": @"story_blocking_mode", @"value": @"block_all" } + ], + [UICommand commandWithTitle:@"Block selected" + image:nil + action:@selector(menuChanged:) + propertyList:@{ @"defaultsKey": @"story_blocking_mode", @"value": @"block_selected" } + ] + ]], + @"story_seen_mode": [UIMenu menuWithChildren:@[ [UICommand commandWithTitle:@"Button" image:nil diff --git a/src/Tweak.x b/src/Tweak.x index 218e253..ac9ec1d 100644 --- a/src/Tweak.x +++ b/src/Tweak.x @@ -62,9 +62,11 @@ BOOL dmVisualMsgsViewedButtonEnabled = false; @"unsent_message_toast": @(NO), @"warn_refresh_clears_preserved": @(NO), @"enable_chat_exclusions": @(YES), + @"chat_blocking_mode": @"block_all", @"exclusions_default_keep_deleted": @(NO), - @"unexclude_inbox_button": @(YES), + @"chat_quick_list_button": @(YES), @"enable_story_user_exclusions": @(YES), + @"story_blocking_mode": @"block_all", @"story_excluded_show_unexclude_eye": @(YES), @"story_seen_mode": @"button", @"story_audio_toggle": @(NO),