diff --git a/FeedFilterSettingsViewController.x b/FeedFilterSettingsViewController.x index f60c82c..7f99f9a 100644 --- a/FeedFilterSettingsViewController.x +++ b/FeedFilterSettingsViewController.x @@ -1,3 +1,4 @@ +#import #import "FeedFilterSettingsViewController.h" #import "DebugMenu.h" @@ -6,6 +7,11 @@ extern UIImage *iconWithName(NSString *iconName); extern Class CoreClass(NSString *name); #define LOC(x, d) (localizedString(x, nil) ?: d) +// Helper to broadcast preferences update +static void postPrefsUpdatedNotification() { + CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), CFSTR("com.level3tjg.redditfilter/prefsUpdated"), NULL, NULL, true); +} + #if REDDITFILTER_DEBUG // Visible declarations for the debug-only helpers so the direct call site in // -cellForRowAtIndexPath: and the @selector(...) references are fully typed. @@ -51,6 +57,10 @@ extern Class CoreClass(NSString *name); case 0: { toggleCell = [tableView dequeueReusableCellWithIdentifier:kToggleCellID forIndexPath:indexPath]; + + // Fixed Switch Handler Accumulation + [toggleCell.accessorySwitch removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; + switch (indexPath.row) { case 0: mainLabelText = LOC(@"filter.settings.promoted.title", @"Promoted"); @@ -132,6 +142,7 @@ extern Class CoreClass(NSString *name); ([cell respondsToSelector:@selector(detailLabel)] ? cell.detailLabel : cell.imageLabelView.detailLabel) .text = detailLabelText; + UIImage *iconImage; for (NSString *iconName in iconNames) { iconImage = iconWithName(iconName); @@ -157,11 +168,14 @@ extern Class CoreClass(NSString *name); layoutGuidance.maxContentWidth - layoutGuidance.gridPaddingDouble, 40.0); [label associatePropertySetter:@selector(setTextColor:) withThemePropertyGetter:@selector(metaTextColor)]; + BaseTableReusableView *headerView = [[%c(BaseTableReusableView) alloc] initWithFrame:CGRectMake(0, 0, tableView.frameWidth, 40.0)]; [headerView.contentView addSubview:label]; + [headerView associatePropertySetter:@selector(setBackgroundColor:) withThemePropertyGetter:@selector(canvasColor)]; + switch (section) { case 0: label.text = [LOC(@"filter.settings.header", @"Filters") uppercaseString]; @@ -190,11 +204,14 @@ extern Class CoreClass(NSString *name); footerHeight); [label associatePropertySetter:@selector(setTextColor:) withThemePropertyGetter:@selector(metaTextColor)]; + BaseTableReusableView *footerView = [[%c(BaseTableReusableView) alloc] initWithFrame:CGRectMake(0, 0, tableView.frameWidth, footerHeight)]; [footerView.contentView addSubview:label]; + [footerView associatePropertySetter:@selector(setBackgroundColor:) withThemePropertyGetter:@selector(canvasColor)]; + switch (section) { case 0: label.text = LOC(@"filter.settings.footer", @"Filter specific types of posts from your feed"); @@ -230,35 +247,44 @@ extern Class CoreClass(NSString *name); forCellReuseIdentifier:kToggleCellID]; [self.tableView registerClass:labelCellClass forCellReuseIdentifier:kLabelCellID]; + #if REDDITFILTER_DEBUG // Debug rows carry multi-line detail text, so let them self-size. self.tableView.estimatedRowHeight = 60.0; self.tableView.rowHeight = UITableViewAutomaticDimension; #endif } + +// Emitting notifications across tweak processes allows updates without querying %new - (void)didTogglePromotedSwitch:(UISwitch *)sender { [NSUserDefaults.standardUserDefaults setBool:!sender.on forKey:kRedditFilterPromoted]; + postPrefsUpdatedNotification(); } %new - (void)didToggleRecommendedSwitch:(UISwitch *)sender { [NSUserDefaults.standardUserDefaults setBool:!sender.on forKey:kRedditFilterRecommended]; + postPrefsUpdatedNotification(); } %new - (void)didToggleNsfwSwitch:(UISwitch *)sender { [NSUserDefaults.standardUserDefaults setBool:!sender.on forKey:kRedditFilterNSFW]; + postPrefsUpdatedNotification(); } %new - (void)didToggleAwardsSwitch:(UISwitch *)sender { [NSUserDefaults.standardUserDefaults setBool:!sender.on forKey:kRedditFilterAwards]; + postPrefsUpdatedNotification(); } %new - (void)didToggleScoresSwitch:(UISwitch *)sender { [NSUserDefaults.standardUserDefaults setBool:!sender.on forKey:kRedditFilterScores]; + postPrefsUpdatedNotification(); } %new - (void)didToggleAutoCollapseAutoModSwitch:(UISwitch *)sender { [NSUserDefaults.standardUserDefaults setBool:sender.on forKey:kRedditFilterAutoCollapseAutoMod]; + postPrefsUpdatedNotification(); } // --------------------------------------------------------------------------- @@ -274,6 +300,7 @@ extern Class CoreClass(NSString *name); - (UITableViewCell *)debugCellForRow:(NSInteger)row inTableView:(UITableView *)tableView { #if REDDITFILTER_DEBUG static NSString *const kRFDebugCellID = @"RFSchemaDebugCell"; + // Deliberately a plain UIKit cell, not a Reddit class: the whole point of // this screen is to keep working when Reddit's own classes/schema change. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kRFDebugCellID]; @@ -289,7 +316,7 @@ extern Class CoreClass(NSString *name); cell.selectionStyle = UITableViewCellSelectionStyleNone; NSArray *snapshot = [[RFSchemaDebug shared] snapshot]; - + // Trailing "reset" row. if (row >= (NSInteger)snapshot.count) { cell.textLabel.textColor = [UIColor systemBlueColor]; diff --git a/Makefile b/Makefile index 5ed8e5e..bfed1c7 100755 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ INSTALL_TARGET_PROCESSES = RedditApp Reddit ARCHS = arm64 -PACKAGE_VERSION = 1.2.3 +PACKAGE_VERSION = 1.2.5 ifdef APP_VERSION PACKAGE_VERSION := $(APP_VERSION)-$(PACKAGE_VERSION) endif diff --git a/Tweak.xm b/Tweak.xm index 98af8bd..fcbb487 100755 --- a/Tweak.xm +++ b/Tweak.xm @@ -6,6 +6,7 @@ #import #import #import +#import #import "Preferences.h" #import "DebugMenu.h" @@ -23,6 +24,23 @@ typedef struct { BOOL automod; } RedditFilterPrefs; +// Optimization 1: Global preferences struct managed via Darwin Notifications +static RedditFilterPrefs globalPrefs; + +static void loadPreferences() { + NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; + globalPrefs.promoted = [defaults objectForKey:kRedditFilterPromoted] ? [defaults boolForKey:kRedditFilterPromoted] : YES; + globalPrefs.recommended = [defaults boolForKey:kRedditFilterRecommended]; + globalPrefs.nsfw = [defaults boolForKey:kRedditFilterNSFW]; + globalPrefs.awards = [defaults boolForKey:kRedditFilterAwards]; + globalPrefs.scores = [defaults boolForKey:kRedditFilterScores]; + globalPrefs.automod = [defaults boolForKey:kRedditFilterAutoCollapseAutoMod]; +} + +static void prefsNotificationCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) { + loadPreferences(); +} + @interface CUICatalog : NSObject { NSBundle *_bundle; } @@ -41,6 +59,16 @@ extern "C" UIImage *iconWithName(NSString *iconName) { UIImage *cachedImage = [imageCache objectForKey:iconName]; if (cachedImage) return cachedImage; + // Optimization 3: Try public UIKit API first to mitigate private API fragility + for (NSBundle *bundle in assetBundles) { + UIImage *image = [UIImage imageNamed:iconName inBundle:bundle compatibleWithTraitCollection:nil]; + if (image) { + [imageCache setObject:image forKey:iconName]; + return image; + } + } + + // Fallback to internal CUICatalog properties if standard retrieval fails for (CUICatalog *catalog in assetCatalogs) { for (NSString *imageName in [catalog allImageNames]) { if ([imageName hasPrefix:iconName] && @@ -52,11 +80,10 @@ extern "C" UIImage *iconWithName(NSString *iconName) { NSBundle *bundle = object_getIvar(catalog, bundleIvar); if (!bundle) continue; - + UIImage *image = [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil]; - if (image) { [imageCache setObject:image forKey:iconName]; return image; @@ -69,11 +96,10 @@ extern "C" UIImage *iconWithName(NSString *iconName) { extern "C" NSString *localizedString(NSString *key, NSString *table) { if (!key) return nil; - NSString *cacheKey = [NSString stringWithFormat:@"%@-%@", key, table ?: @"nil"]; NSString *cachedString = [stringCache objectForKey:cacheKey]; if (cachedString) return cachedString; - + for (NSBundle *bundle in assetBundles) { NSString *localizedString = [bundle localizedStringForKey:key value:nil table:table]; if (![localizedString isEqualToString:key]) { @@ -101,18 +127,17 @@ extern "C" Class CoreClass(NSString *name) { } static BOOL shouldFilterObject(id object) { - // Optimization: Check preferences first before doing expensive class/selector introspection - NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; - BOOL filterPromoted = [defaults boolForKey:kRedditFilterPromoted]; - BOOL filterRecommended = [defaults boolForKey:kRedditFilterRecommended]; - BOOL filterNSFW = [defaults boolForKey:kRedditFilterNSFW]; + // Optimization 1: Use globally cached preferences + BOOL filterPromoted = globalPrefs.promoted; + BOOL filterRecommended = globalPrefs.recommended; + BOOL filterNSFW = globalPrefs.nsfw; // If no relevant filters are on, return early if (!filterPromoted && !filterRecommended && !filterNSFW) return NO; // Do introspection NSString *className = NSStringFromClass(object_getClass(object)); - + // 1. Check Promoted (Ads) if (filterPromoted) { BOOL isAdPost = [className hasSuffix:@"AdPost"] || @@ -141,12 +166,12 @@ static NSArray *filteredObjects(NSArray *objects) { return [objects filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( id object, NSDictionary *bindings) { return !shouldFilterObject(object); - }]]; + }]]; } static void filterNode(NSMutableDictionary *node, RedditFilterPrefs prefs) { if (![node isKindOfClass:NSMutableDictionary.class]) return; - + // Fetch typeName once and ensure it is a valid string to prevent unrecognized selector crashes NSString *typeName = node[@"__typename"]; if (![typeName isKindOfClass:NSString.class]) return; @@ -165,7 +190,6 @@ static void filterNode(NSMutableDictionary *node, RedditFilterPrefs prefs) { node[@"isGildable"] = @NO; } if (prefs.scores) node[@"isScoreHidden"] = @YES; - if (prefs.automod) { NSDictionary *authorInfo = node[@"authorInfo"]; if ([authorInfo isKindOfClass:NSDictionary.class]) { @@ -185,24 +209,25 @@ static void filterNode(NSMutableDictionary *node, RedditFilterPrefs prefs) { } // 2. Check Recommended - if (prefs.recommended && [node[@"recommendationContext"] isKindOfClass:NSDictionary.class]) { - NSDictionary *recContext = node[@"recommendationContext"]; - id recTypeName = recContext[@"typeName"]; - id typeIdentifier = recContext[@"typeIdentifier"]; - id isContextHidden = recContext[@"isContextHidden"]; - - if ([recTypeName isKindOfClass:NSString.class] && - [typeIdentifier isKindOfClass:NSString.class] && - [isContextHidden isKindOfClass:NSNumber.class]) { - - if (!(([recTypeName isEqualToString:@"PopularRecommendationContext"] || - [typeIdentifier hasPrefix:@"global_popular"]) && - [isContextHidden boolValue])) { - node[@"cells"] = @[]; - return; // Exit early if we cleared the cells - } - } - } + if (prefs.recommended && [node[@"recommendationContext"] isKindOfClass:NSDictionary.class]) { + NSDictionary *recContext = node[@"recommendationContext"]; + id recTypeName = recContext[@"typeName"]; + id typeIdentifier = recContext[@"typeIdentifier"]; + + if ([recTypeName isKindOfClass:NSString.class] && + [typeIdentifier isKindOfClass:NSString.class]) { + + // Check if the post is part of the standard Popular feed + BOOL isPopularFeed = [recTypeName isEqualToString:@"PopularRecommendationContext"] || + [typeIdentifier hasPrefix:@"global_popular"]; + + // If it's NOT the popular feed, it is a home-feed recommendation. Wipe it. + if (!isPopularFeed) { + node[@"cells"] = @[]; + return; + } + } + } // 3. Process remaining ActionCells ONLY if Awards or Scores filters are enabled if (prefs.awards || prefs.scores) { @@ -210,7 +235,6 @@ static void filterNode(NSMutableDictionary *node, RedditFilterPrefs prefs) { if ([cells isKindOfClass:NSMutableArray.class]) { for (NSMutableDictionary *cell in cells) { if (![cell isKindOfClass:NSMutableDictionary.class]) continue; - if ([cell[@"__typename"] isEqualToString:@"ActionCell"]) { if (prefs.awards) { cell[@"isAwardHidden"] = @YES; @@ -232,20 +256,19 @@ static void filterNode(NSMutableDictionary *node, RedditFilterPrefs prefs) { // Generic, schema-agnostic filtering. Used for unknown operations and, now, // as the fallback whenever a known operation's hardcoded fast path fails to -// resolve (e.g. after Reddit renames part of its GraphQL schema). Because it -// walks the response structurally instead of by a fixed key path, it keeps -// working across the common "an ancestor key got renamed" breakage. +// resolve (e.g. after Reddit renames part of its GraphQL schema). static void filterGenericResponse(NSMutableDictionary *json, RedditFilterPrefs prefs) { if (![json[@"data"] isKindOfClass:NSDictionary.class]) return; NSDictionary *dataDict = json[@"data"]; id root = dataDict.allValues.firstObject; - + if ([root isKindOfClass:NSDictionary.class]) { NSMutableDictionary *rootDict = (NSMutableDictionary *)root; - + // Read the first child once instead of re-evaluating allValues repeatedly id firstChild = rootDict.allValues.firstObject; + if ([firstChild isKindOfClass:NSDictionary.class]) { id edges = ((NSDictionary *)firstChild)[@"edges"]; if ([edges isKindOfClass:NSArray.class]) { @@ -265,17 +288,19 @@ static void filterGenericResponse(NSMutableDictionary *json, RedditFilterPrefs p } } - NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; - BOOL filterPromoted = [defaults boolForKey:kRedditFilterPromoted]; - - if (filterPromoted && rootDict[@"commentsPageAds"]) + // Optimization 1 & 4: Reuse passed preferences + if (prefs.promoted && rootDict[@"commentsPageAds"]) rootDict[@"commentsPageAds"] = @[]; - if (filterPromoted && rootDict[@"commentTreeAds"]) + + if (prefs.promoted && rootDict[@"commentTreeAds"]) rootDict[@"commentTreeAds"] = @[]; - if (filterPromoted && rootDict[@"pdpCommentsAds"]) // Kept just in case the fast path misses + + if (prefs.promoted && rootDict[@"pdpCommentsAds"]) // Kept just in case the fast path misses rootDict[@"pdpCommentsAds"] = @[]; - if (rootDict[@"recommendations"] && [defaults boolForKey:kRedditFilterRecommended]) + + if (rootDict[@"recommendations"] && prefs.recommended) rootDict[@"recommendations"] = @[]; + } else if ([root isKindOfClass:NSArray.class]) { for (NSMutableDictionary *node in (NSArray *)root) filterNode(node, prefs); @@ -300,33 +325,19 @@ static void filterGenericResponse(NSMutableDictionary *json, RedditFilterPrefs p // Safe bail-out to avoid executing NSJSONSerialization on empty/broken payloads if (error || !data || data.length == 0) return completionHandler(data, response, error); - NSError *jsonError = nil; - id jsonObject = [NSJSONSerialization JSONObjectWithData:data - options:NSJSONReadingMutableContainers - error:&jsonError]; - - if (jsonError || !jsonObject || ![jsonObject isKindOfClass:NSDictionary.class]) { - return completionHandler(data, response, error); - } - - NSMutableDictionary *json = (NSMutableDictionary *)jsonObject; - // Load preferences once per network request - NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; - RedditFilterPrefs prefs = { - [defaults boolForKey:kRedditFilterPromoted], - [defaults boolForKey:kRedditFilterRecommended], - [defaults boolForKey:kRedditFilterNSFW], - [defaults boolForKey:kRedditFilterAwards], - [defaults boolForKey:kRedditFilterScores], - [defaults boolForKey:kRedditFilterAutoCollapseAutoMod] - }; - - // Identify the GraphQL Operation + // Identify the GraphQL Operation Safely (Optimization 2) NSString *operationName = @"Unknown"; + if (request.HTTPBody) { - NSDictionary *bodyJson = [NSJSONSerialization JSONObjectWithData:request.HTTPBody options:0 error:nil]; - if (bodyJson[@"id"]) operationName = bodyJson[@"id"]; - else if (bodyJson[@"operationName"]) operationName = bodyJson[@"operationName"]; + NSString *bodyString = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding]; + if (bodyString) { + // Avoid NSJSONSerialization, extract operationName directly using regex + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\"(?:operationName|id)\"\\s*:\\s*\"([^\"]+)\"" options:0 error:nil]; + NSTextCheckingResult *match = [regex firstMatchInString:bodyString options:0 range:NSMakeRange(0, bodyString.length)]; + if (match && match.numberOfRanges > 1) { + operationName = [bodyString substringWithRange:[match rangeAtIndex:1]]; + } + } } else if ([request.URL.query containsString:@"operationName="]) { NSArray *components = [request.URL.query componentsSeparatedByString:@"&"]; for (NSString *param in components) { @@ -337,19 +348,31 @@ static void filterGenericResponse(NSMutableDictionary *json, RedditFilterPrefs p } } - // Ignore Telemetry & Configs (Performance Saver) + // Ignore Telemetry & Configs (Performance Saver - Placed BEFORE response deserialization!) if ([ignoredOperationsSet containsObject:operationName]) { return completionHandler(data, response, error); } - // Fast path based on known schemas. Each branch tries its hardcoded - // schema path; on a miss it records the failure (debug builds also try - // to auto-discover where the data moved) and falls back to the generic - // structural filter so content keeps being filtered. + NSError *jsonError = nil; + id jsonObject = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&jsonError]; + + if (jsonError || !jsonObject || ![jsonObject isKindOfClass:NSDictionary.class]) { + return completionHandler(data, response, error); + } + + NSMutableDictionary *json = (NSMutableDictionary *)jsonObject; + + // Optimization 1: Use globally cached preferences + RedditFilterPrefs prefs = globalPrefs; + + // Fast path based on known schemas. if ([operationName isEqualToString:@"HomeFeedSdui"]) { id edges = [json valueForKeyPath:@"data.homeV3.elements.edges"]; BOOL resolved = [edges isKindOfClass:NSArray.class]; RF_RECORD_SCHEMA(@"HomeFeedSdui", @"data.homeV3.elements.edges", resolved, json, RFSchemaSigEdges); + if (resolved) { for (NSMutableDictionary *edge in (NSArray *)edges) filterNode(edge[@"node"], prefs); @@ -360,6 +383,7 @@ static void filterGenericResponse(NSMutableDictionary *json, RedditFilterPrefs p id edges = [json valueForKeyPath:@"data.popularV3.elements.edges"]; BOOL resolved = [edges isKindOfClass:NSArray.class]; RF_RECORD_SCHEMA(@"PopularFeedSdui", @"data.popularV3.elements.edges", resolved, json, RFSchemaSigEdges); + if (resolved) { for (NSMutableDictionary *edge in (NSArray *)edges) filterNode(edge[@"node"], prefs); @@ -370,6 +394,7 @@ static void filterGenericResponse(NSMutableDictionary *json, RedditFilterPrefs p id nodes = [json valueForKeyPath:@"data.postsInfoByIds"]; BOOL resolved = [nodes isKindOfClass:NSArray.class]; RF_RECORD_SCHEMA(@"FeedPostDetailsByIds", @"data.postsInfoByIds", resolved, json, RFSchemaSigNodeArray); + if (resolved) { for (NSMutableDictionary *node in (NSArray *)nodes) filterNode(node, prefs); @@ -383,6 +408,7 @@ static void filterGenericResponse(NSMutableDictionary *json, RedditFilterPrefs p BOOL resolved = [trees isKindOfClass:NSArray.class] || ([postInfo isKindOfClass:NSDictionary.class] && postInfo[@"commentForest"] == nil); RF_RECORD_SCHEMA(@"PostInfoById", @"data.postInfoById.commentForest.trees", resolved, json, RFSchemaSigTrees); + if (resolved) { if ([trees isKindOfClass:NSArray.class]) { for (NSMutableDictionary *tree in (NSArray *)trees) @@ -407,7 +433,8 @@ static void filterGenericResponse(NSMutableDictionary *json, RedditFilterPrefs p } BOOL resolved = (adContainer != nil); RF_RECORD_SCHEMA(@"PdpCommentsAds", @"data.*.pdpCommentsAds", resolved, json, RFSchemaSigCommentsAds); - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted]) { + + if (prefs.promoted) { if (resolved) { adContainer[@"pdpCommentsAds"] = @[]; } else { @@ -440,68 +467,70 @@ static void filterGenericResponse(NSMutableDictionary *json, RedditFilterPrefs p %hook FeedNetworkSource - (NSArray *)postsAndCommentsFromData:(id)data { - return filteredObjects(%orig); + NSArray *origArray = %orig; + return filteredObjects(origArray); } %end %hook PostDetailPresenter - (BOOL)shouldFetchCommentAdPost { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted] ? NO : %orig; + return globalPrefs.promoted ? NO : %orig; } - (BOOL)shouldFetchAdditionalCommentAdPosts { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted] ? NO : %orig; + return globalPrefs.promoted ? NO : %orig; } %end %hook Carousel - (BOOL)isHiddenByUserWithAccountSettings:(id)accountSettings { - return ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterRecommended] && + return (globalPrefs.recommended && ([self.analyticType containsString:@"recommended"] || [self.analyticType containsString:@"similar"] || - [self.analyticType containsString:@"popular"])) || %orig; + [self.analyticType containsString:@"popular"])) || + %orig; } %end %hook QuickActionViewModel - (void)fetchActions { - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterRecommended]) return; + if (globalPrefs.recommended) return; %orig; } %end %hook Post - (NSArray *)awardingTotals { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? nil : %orig; + return globalPrefs.awards ? nil : %orig; } - (NSUInteger)totalAwardsReceived { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? 0 : %orig; + return globalPrefs.awards ? 0 : %orig; } - (BOOL)canAward { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? NO : %orig; + return globalPrefs.awards ? NO : %orig; } - (BOOL)isScoreHidden { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterScores] ? YES : %orig; + return globalPrefs.scores ? YES : %orig; } %end %hook Comment - (NSArray *)awardingTotals { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? nil : %orig; + return globalPrefs.awards ? nil : %orig; } - (NSUInteger)totalAwardsReceived { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? 0 : %orig; + return globalPrefs.awards ? 0 : %orig; } - (BOOL)canAward { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? NO : %orig; + return globalPrefs.awards ? NO : %orig; } - (BOOL)shouldHighlightForHighAward { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? NO : %orig; + return globalPrefs.awards ? NO : %orig; } - (BOOL)isScoreHidden { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterScores] ? YES : %orig; + return globalPrefs.scores ? YES : %orig; } - (BOOL)shouldAutoCollapse { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAutoCollapseAutoMod] && + return globalPrefs.automod && [((Comment *)self).authorPk isEqualToString:@"t2_6l4z3"] ? YES : %orig; @@ -514,11 +543,10 @@ static char kConstraintsAddedKey; %hook ToggleImageTableViewCell - (void)updateConstraints { %orig; - // Prevent adding duplicate constraints if updateConstraints is called multiple times. NSNumber *constraintsAdded = objc_getAssociatedObject(self, &kConstraintsAddedKey); if (constraintsAdded.boolValue) return; - + UIStackView *horizontalStackView = [self respondsToSelector:@selector(imageLabelView)] ? [self imageLabelView].horizontalStackView : object_getIvar(self, class_getInstanceVariable(object_getClass(self), "horizontalStackView")); @@ -526,7 +554,7 @@ static char kConstraintsAddedKey; UILabel *detailLabel = [self respondsToSelector:@selector(imageLabelView)] ? [self imageLabelView].detailLabel : [self detailLabel]; - + if (!horizontalStackView || !detailLabel) return; if (detailLabel.text) { @@ -554,7 +582,6 @@ static char kConstraintsAddedKey; multiplier:1 constant:0] ]]; - // Mark as added objc_setAssociatedObject(self, &kConstraintsAddedKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @@ -567,7 +594,7 @@ static char kConstraintsAddedKey; // Initialize caches imageCache = [[NSCache alloc] init]; stringCache = [[NSCache alloc] init]; - + // Initialize Ignored Operations Set ignoredOperationsSet = [[NSSet alloc] initWithObjects: @"GetAccount", @"FetchIdentityPreferences", @"DynamicConfigsByNames", @@ -595,14 +622,12 @@ static char kConstraintsAddedKey; for (NSString *file in [NSFileManager.defaultManager contentsOfDirectoryAtPath:[NSBundle.mainBundle.bundlePath stringByAppendingPathComponent:@"Frameworks"] error:nil]) { if (![file hasSuffix:@"framework"]) continue; - NSString *frameworkPath = [NSBundle.mainBundle pathForResource:[file stringByDeletingPathExtension] ofType:@"framework" inDirectory:@"Frameworks"]; NSBundle *bundle = [NSBundle bundleWithPath:frameworkPath]; if (bundle) [assetBundles addObject:bundle]; - + for (NSString *file in [NSFileManager.defaultManager contentsOfDirectoryAtPath:frameworkPath error:nil]) { if (![file hasSuffix:@"bundle"]) continue; - NSBundle *bundle = [NSBundle bundleWithPath:[frameworkPath stringByAppendingPathComponent:file]]; if (bundle) [assetBundles addObject:bundle]; } @@ -614,26 +639,25 @@ static char kConstraintsAddedKey; if (!error) [assetCatalogs addObject:catalog]; } - // Correct keys used for default values. Previously all checks were for kRedditFilterPromoted. + // Correct keys used for default values. NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; if (![defaults objectForKey:kRedditFilterPromoted]) [defaults setBool:true forKey:kRedditFilterPromoted]; - if (![defaults objectForKey:kRedditFilterRecommended]) [defaults setBool:false forKey:kRedditFilterRecommended]; - if (![defaults objectForKey:kRedditFilterNSFW]) [defaults setBool:false forKey:kRedditFilterNSFW]; - if (![defaults objectForKey:kRedditFilterAwards]) [defaults setBool:false forKey:kRedditFilterAwards]; - if (![defaults objectForKey:kRedditFilterScores]) [defaults setBool:false forKey:kRedditFilterScores]; - if (![defaults objectForKey:kRedditFilterAutoCollapseAutoMod]) [defaults setBool:false forKey:kRedditFilterAutoCollapseAutoMod]; + + // Set up cached struct & notification listener + loadPreferences(); + CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, prefsNotificationCallback, CFSTR("com.level3tjg.redditfilter/prefsUpdated"), NULL, CFNotificationSuspensionBehaviorCoalesce); %init; %init(Legacy, Comment = CoreClass(@"Comment"), Post = CoreClass(@"Post"),