mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-04-30 16:17:59 +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:
@@ -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 **\***
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -404,7 +404,9 @@ typedef NS_ENUM(NSInteger, SCIBackupPreviewRowKind) {
|
||||
+ (NSArray<NSString *> *)extraDataKeys {
|
||||
return @[
|
||||
@"excluded_threads",
|
||||
@"included_threads",
|
||||
@"excluded_story_users",
|
||||
@"included_story_users",
|
||||
@"embed_custom_domains",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-1
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user