From 0c3b050ab6a8be049181623f784704451a232541 Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:09:53 -0500 Subject: [PATCH 01/10] Update Tweak.xm --- Tweak.xm | 388 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 225 insertions(+), 163 deletions(-) diff --git a/Tweak.xm b/Tweak.xm index 4f27973..4e0230c 100755 --- a/Tweak.xm +++ b/Tweak.xm @@ -8,6 +8,10 @@ #import #import "Preferences.h" +// --- Cache Setup --- +static NSCache *imageCache; +static NSCache *stringCache; + @interface CUICatalog : NSObject { NSBundle *_bundle; } @@ -20,24 +24,46 @@ static NSMutableArray *assetBundles; static NSMutableArray *assetCatalogs; extern "C" UIImage *iconWithName(NSString *iconName) { - for (CUICatalog *catalog in assetCatalogs) - for (NSString *imageName in [catalog allImageNames]) - if ([imageName hasPrefix:iconName] && - (imageName.length == iconName.length || imageName.length == iconName.length + 3)) - return [UIImage imageNamed:imageName - inBundle:object_getIvar(catalog, - class_getInstanceVariable( - object_getClass(catalog), "_bundle")) - compatibleWithTraitCollection:nil]; - return nil; + if (!iconName) return nil; + + // Check Cache First + UIImage *cachedImage = [imageCache objectForKey:iconName]; + if (cachedImage) return cachedImage; + + for (CUICatalog *catalog in assetCatalogs) { + for (NSString *imageName in [catalog allImageNames]) { + if ([imageName hasPrefix:iconName] && + (imageName.length == iconName.length || imageName.length == iconName.length + 3)) { + + UIImage *image = [UIImage imageNamed:imageName + inBundle:object_getIvar(catalog, class_getInstanceVariable(object_getClass(catalog), "_bundle")) + compatibleWithTraitCollection:nil]; + + if (image) { + [imageCache setObject:image forKey:iconName]; + return image; + } + } + } + } + return nil; } extern "C" NSString *localizedString(NSString *key, NSString *table) { - for (NSBundle *bundle in assetBundles) { - NSString *localizedString = [bundle localizedStringForKey:key value:nil table:table]; - if (![localizedString isEqualToString:key]) return localizedString; - } - return nil; + 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]) { + [stringCache setObject:localizedString forKey:cacheKey]; + return localizedString; + } + } + return nil; } extern "C" Class CoreClass(NSString *name) { @@ -57,68 +83,100 @@ extern "C" Class CoreClass(NSString *name) { } static BOOL shouldFilterObject(id object) { - NSString *className = NSStringFromClass(object_getClass(object)); - BOOL isAdPost = [className hasSuffix:@"AdPost"] || - ([object respondsToSelector:@selector(isAdPost)] && ((Post *)object).isAdPost) || - ([object respondsToSelector:@selector(isPromotedUserPostAd)] && - [(Post *)object isPromotedUserPostAd]) || - ([object respondsToSelector:@selector(isPromotedCommunityPostAd)] && - [(Post *)object isPromotedCommunityPostAd]); - BOOL isRecommendation = [className containsString:@"Recommend"]; - BOOL isNSFW = [object respondsToSelector:@selector(isNSFW)] && ((Post *)object).isNSFW; - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted] && isAdPost) - return YES; - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterRecommended] && isRecommendation) - return YES; - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterNSFW] && isNSFW) return YES; - return NO; + // 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]; + + // 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"] || + ([object respondsToSelector:@selector(isAdPost)] && ((Post *)object).isAdPost) || + ([object respondsToSelector:@selector(isPromotedUserPostAd)] && [(Post *)object isPromotedUserPostAd]) || + ([object respondsToSelector:@selector(isPromotedCommunityPostAd)] && [(Post *)object isPromotedCommunityPostAd]); + if (isAdPost) return YES; + } + + // 2. Check Recommended + if (filterRecommended) { + BOOL isRecommendation = [className containsString:@"Recommend"]; + if (isRecommendation) return YES; + } + + // 3. Check NSFW + if (filterNSFW) { + BOOL isNSFW = [object respondsToSelector:@selector(isNSFW)] && ((Post *)object).isNSFW; + if (isNSFW) return YES; + } + + return NO; } static NSArray *filteredObjects(NSArray *objects) { return [objects filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( id object, NSDictionary *bindings) { - return !shouldFilterObject(object); - }]]; + return !shouldFilterObject(object); + }]]; } static void filterNode(NSMutableDictionary *node) { if (![node isKindOfClass:NSMutableDictionary.class]) return; + + NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; + // Regular post if ([node[@"__typename"] isEqualToString:@"SubredditPost"]) { - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards]) { + if ([defaults boolForKey:kRedditFilterAwards]) { node[@"awardings"] = @[]; node[@"isGildable"] = @NO; } - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterScores]) + if ([defaults boolForKey:kRedditFilterScores]) node[@"isScoreHidden"] = @YES; - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterNSFW] && - [node[@"isNsfw"] boolValue]) + if ([defaults boolForKey:kRedditFilterNSFW] && [node[@"isNsfw"] boolValue]) node[@"isHidden"] = @YES; } + // CellGroup handling if ([node[@"__typename"] isEqualToString:@"CellGroup"]) { - for (NSMutableDictionary *cell in node[@"cells"]) { - if ([cell[@"__typename"] isEqualToString:@"ActionCell"]) { - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards]) { - cell[@"isAwardHidden"] = @YES; - // Fix: Check for NSNull before accessing nested dictionary - id goldenUpvoteInfo = cell[@"goldenUpvoteInfo"]; - if ([goldenUpvoteInfo isKindOfClass:NSDictionary.class] && - ![goldenUpvoteInfo isEqual:[NSNull null]]) { - cell[@"goldenUpvoteInfo"][@"isGildable"] = @NO; + // Helper to filter cells + NSMutableArray *cells = node[@"cells"]; + if ([cells isKindOfClass:[NSMutableArray class]]) { + for (NSMutableDictionary *cell in cells) { + if (![cell isKindOfClass:NSMutableDictionary.class]) continue; + + if ([cell[@"__typename"] isEqualToString:@"ActionCell"]) { + if ([defaults boolForKey:kRedditFilterAwards]) { + cell[@"isAwardHidden"] = @YES; + id goldenUpvoteInfo = cell[@"goldenUpvoteInfo"]; + if ([goldenUpvoteInfo isKindOfClass:NSDictionary.class] && + ![goldenUpvoteInfo isEqual:[NSNull null]]) { + // Ensure we can mutate it, though usually JSON deserialization with MutableContainers handles this + if ([goldenUpvoteInfo isKindOfClass:NSMutableDictionary.class]) { + ((NSMutableDictionary *)goldenUpvoteInfo)[@"isGildable"] = @NO; + } + } + } + if ([defaults boolForKey:kRedditFilterScores]) + cell[@"isScoreHidden"] = @YES; } } - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterScores]) - cell[@"isScoreHidden"] = @YES; - } } + // Check for ads in CellGroup - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted] && + if ([defaults boolForKey:kRedditFilterPromoted] && [node[@"adPayload"] isKindOfClass:NSDictionary.class]) { node[@"cells"] = @[]; } + // Check for recommendations in CellGroup - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterRecommended] && + if ([defaults boolForKey:kRedditFilterRecommended] && ![node[@"recommendationContext"] isEqual:[NSNull null]] && [node[@"recommendationContext"] isKindOfClass:NSDictionary.class]) { NSDictionary *recommendationContext = node[@"recommendationContext"]; @@ -139,22 +197,22 @@ static void filterNode(NSMutableDictionary *node) { } } // Ad post - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted]) { + if ([defaults boolForKey:kRedditFilterPromoted]) { if ([node[@"__typename"] isEqualToString:@"AdPost"]) { node[@"isHidden"] = @YES; } } // Comment if ([node[@"__typename"] isEqualToString:@"Comment"]) { - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards]) { + if ([defaults boolForKey:kRedditFilterAwards]) { node[@"awardings"] = @[]; node[@"isGildable"] = @NO; } - if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterScores]) + if ([defaults boolForKey:kRedditFilterScores]) node[@"isScoreHidden"] = @YES; if ([node[@"authorInfo"] isKindOfClass:NSDictionary.class] && [node[@"authorInfo"][@"id"] isEqualToString:@"t2_6l4z3"] && - [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAutoCollapseAutoMod]) + [defaults boolForKey:kRedditFilterAutoCollapseAutoMod]) node[@"isInitiallyCollapsed"] = @YES; } } @@ -165,50 +223,55 @@ static void filterNode(NSMutableDictionary *node) { NSError *error))completionHandler { if (![request.URL.host hasPrefix:@"gql"] && ![request.URL.host hasPrefix:@"oauth"]) return %orig; + void (^newCompletionHandler)(NSData *, NSURLResponse *, NSError *) = ^(NSData *data, NSURLResponse *response, NSError *error) { if (error || !data) return completionHandler(data, response, error); - NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data - options:NSJSONReadingMutableContainers - error:&error]; - if (error || !json) return completionHandler(data, response, error); - if ([json isKindOfClass:NSDictionary.class]) { - if (json[@"data"] && [json[@"data"] isKindOfClass:NSDictionary.class]) { - NSDictionary *data = json[@"data"]; - NSMutableDictionary *root = data.allValues.firstObject; + + 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; + + if (json[@"data"] && [json[@"data"] isKindOfClass:NSDictionary.class]) { + NSDictionary *dataDict = json[@"data"]; + NSMutableDictionary *root = dataDict.allValues.firstObject; + if ([root isKindOfClass:NSDictionary.class]) { if ([root.allValues.firstObject isKindOfClass:NSDictionary.class] && root.allValues.firstObject[@"edges"]) for (NSMutableDictionary *edge in root.allValues.firstObject[@"edges"]) filterNode(edge[@"node"]); - + if (root[@"commentForest"]) for (NSMutableDictionary *tree in root[@"commentForest"][@"trees"]) filterNode(tree[@"node"]); - - if (root[@"commentsPageAds"] && - [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted]) + + NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; + BOOL filterPromoted = [defaults boolForKey:kRedditFilterPromoted]; + + if (root[@"commentsPageAds"] && filterPromoted) root[@"commentsPageAds"] = @[]; - - if (root[@"commentTreeAds"] && - [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted]) + if (root[@"commentTreeAds"] && filterPromoted) root[@"commentTreeAds"] = @[]; - - if (root[@"pdpCommentsAds"] && - [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted]) + if (root[@"pdpCommentsAds"] && filterPromoted) root[@"pdpCommentsAds"] = @[]; - - if (root[@"recommendations"] && - [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterRecommended]) + if (root[@"recommendations"] && [defaults boolForKey:kRedditFilterRecommended]) root[@"recommendations"] = @[]; - + } else if ([root isKindOfClass:NSArray.class]) { for (NSMutableDictionary *node in (NSArray *)root) filterNode(node); } - } } - data = [NSJSONSerialization dataWithJSONObject:json options:0 error:nil]; - completionHandler(data, response, error); + + NSData *modifiedData = [NSJSONSerialization dataWithJSONObject:json options:0 error:nil]; + completionHandler(modifiedData ?: data, response, error); }; return %orig(request, newCompletionHandler); } @@ -234,12 +297,10 @@ static void filterNode(NSMutableDictionary *node) { %hook PostDetailPresenter - (BOOL)shouldFetchCommentAdPost { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted] ? NO - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted] ? NO : %orig; } - (BOOL)shouldFetchAdditionalCommentAdPosts { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted] ? NO - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted] ? NO : %orig; } %end @@ -248,8 +309,7 @@ static void filterNode(NSMutableDictionary *node) { return ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterRecommended] && ([self.analyticType containsString:@"recommended"] || [self.analyticType containsString:@"similar"] || - [self.analyticType containsString:@"popular"])) || - %orig; + [self.analyticType containsString:@"popular"])) || %orig; } %end @@ -262,43 +322,34 @@ static void filterNode(NSMutableDictionary *node) { %hook Post - (NSArray *)awardingTotals { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? nil - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? nil : %orig; } - (NSUInteger)totalAwardsReceived { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? 0 - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? 0 : %orig; } - (BOOL)canAward { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? NO - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? NO : %orig; } - (BOOL)isScoreHidden { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterScores] ? YES - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterScores] ? YES : %orig; } %end %hook Comment - (NSArray *)awardingTotals { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? nil - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? nil : %orig; } - (NSUInteger)totalAwardsReceived { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? 0 - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? 0 : %orig; } - (BOOL)canAward { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? NO - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? NO : %orig; } - (BOOL)shouldHighlightForHighAward { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? NO - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAwards] ? NO : %orig; } - (BOOL)isScoreHidden { - return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterScores] ? YES - : %orig; + return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterScores] ? YES : %orig; } - (BOOL)shouldAutoCollapse { return [NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterAutoCollapseAutoMod] && @@ -310,103 +361,114 @@ static void filterNode(NSMutableDictionary *node) { %hook ToggleImageTableViewCell - (void)updateConstraints { - %orig; - UIStackView *horizontalStackView = - [self respondsToSelector:@selector(imageLabelView)] + %orig; + + // Fix: Prevent adding duplicate constraints if updateConstraints is called multiple times. + // Use an associated object to track if we've already done this. + NSNumber *constraintsAdded = objc_getAssociatedObject(self, @selector(updateConstraints)); + if (constraintsAdded.boolValue) return; + + UIStackView *horizontalStackView = [self respondsToSelector:@selector(imageLabelView)] ? [self imageLabelView].horizontalStackView - : object_getIvar(self, - class_getInstanceVariable(object_getClass(self), "horizontalStackView")); - UILabel *detailLabel = [self respondsToSelector:@selector(imageLabelView)] + : object_getIvar(self, class_getInstanceVariable(object_getClass(self), "horizontalStackView")); + + UILabel *detailLabel = [self respondsToSelector:@selector(imageLabelView)] ? [self imageLabelView].detailLabel : [self detailLabel]; - if (!horizontalStackView || !detailLabel) return; - if (detailLabel.text) { - UIView *contentView = [self contentView]; - [contentView addConstraints:@[ - [NSLayoutConstraint constraintWithItem:detailLabel - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:horizontalStackView - attribute:NSLayoutAttributeHeight - multiplier:.33 - constant:0], - [NSLayoutConstraint constraintWithItem:horizontalStackView - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:contentView - attribute:NSLayoutAttributeHeight - multiplier:1 - constant:0], - [NSLayoutConstraint constraintWithItem:horizontalStackView - attribute:NSLayoutAttributeCenterY - relatedBy:NSLayoutRelationEqual - toItem:contentView - attribute:NSLayoutAttributeCenterY - multiplier:1 - constant:0] - ]]; - } + + if (!horizontalStackView || !detailLabel) return; + + if (detailLabel.text) { + UIView *contentView = [self contentView]; + [contentView addConstraints:@[ + [NSLayoutConstraint constraintWithItem:detailLabel + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:horizontalStackView + attribute:NSLayoutAttributeHeight + multiplier:.33 + constant:0], + [NSLayoutConstraint constraintWithItem:horizontalStackView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:contentView + attribute:NSLayoutAttributeHeight + multiplier:1 + constant:0], + [NSLayoutConstraint constraintWithItem:horizontalStackView + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:contentView + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0] + ]]; + + // Mark as added + objc_setAssociatedObject(self, @selector(updateConstraints), @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } } %end %end %ctor { + // Initialize caches + imageCache = [[NSCache alloc] init]; + stringCache = [[NSCache alloc] init]; + assetBundles = [NSMutableArray array]; assetCatalogs = [NSMutableArray array]; [assetBundles addObject:NSBundle.mainBundle]; - for (NSString *file in - [NSFileManager.defaultManager contentsOfDirectoryAtPath:NSBundle.mainBundle.bundlePath - error:nil]) { + + for (NSString *file in [NSFileManager.defaultManager contentsOfDirectoryAtPath:NSBundle.mainBundle.bundlePath error:nil]) { if (![file hasSuffix:@"bundle"]) continue; - NSBundle *bundle = [NSBundle - bundleWithPath:[NSBundle.mainBundle pathForResource:[file stringByDeletingPathExtension] - ofType:@"bundle"]]; + NSBundle *bundle = [NSBundle bundleWithPath:[NSBundle.mainBundle pathForResource:[file stringByDeletingPathExtension] ofType:@"bundle"]]; if (bundle) [assetBundles addObject:bundle]; } - for (NSString *file in [NSFileManager.defaultManager - contentsOfDirectoryAtPath:[NSBundle.mainBundle.bundlePath - stringByAppendingPathComponent:@"Frameworks"] - error:nil]) { + + 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"]; + 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]) { + for (NSString *file in [NSFileManager.defaultManager contentsOfDirectoryAtPath:frameworkPath error:nil]) { if (![file hasSuffix:@"bundle"]) continue; - NSBundle *bundle = - [NSBundle bundleWithPath:[frameworkPath stringByAppendingPathComponent:file]]; - + NSBundle *bundle = [NSBundle bundleWithPath:[frameworkPath stringByAppendingPathComponent:file]]; if (bundle) [assetBundles addObject:bundle]; } } + for (NSBundle *bundle in assetBundles) { NSError *error; - CUICatalog *catalog = [[%c(CUICatalog) alloc] initWithName:@"Assets" - fromBundle:bundle - error:&error]; + CUICatalog *catalog = [[%c(CUICatalog) alloc] initWithName:@"Assets" fromBundle:bundle error:&error]; if (!error) [assetCatalogs addObject:catalog]; } + + // Fix: Correct keys used for default values. Previously all checks were for kRedditFilterPromoted. NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; + if (![defaults objectForKey:kRedditFilterPromoted]) [defaults setBool:true forKey:kRedditFilterPromoted]; - if (![defaults objectForKey:kRedditFilterPromoted]) + + if (![defaults objectForKey:kRedditFilterRecommended]) [defaults setBool:false forKey:kRedditFilterRecommended]; - if (![defaults objectForKey:kRedditFilterPromoted]) + + if (![defaults objectForKey:kRedditFilterNSFW]) [defaults setBool:false forKey:kRedditFilterNSFW]; - if (![defaults objectForKey:kRedditFilterPromoted]) + + if (![defaults objectForKey:kRedditFilterAwards]) [defaults setBool:false forKey:kRedditFilterAwards]; - if (![defaults objectForKey:kRedditFilterPromoted]) + + if (![defaults objectForKey:kRedditFilterScores]) [defaults setBool:false forKey:kRedditFilterScores]; - if (![defaults objectForKey:kRedditFilterPromoted]) + + if (![defaults objectForKey:kRedditFilterAutoCollapseAutoMod]) [defaults setBool:false forKey:kRedditFilterAutoCollapseAutoMod]; + %init; %init(Legacy, Comment = CoreClass(@"Comment"), Post = CoreClass(@"Post"), QuickActionViewModel = CoreClass(@"QuickActionViewModel"), From 6138fdc74b38d8e2701310a61a12153fe5e39d3c Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:26:27 -0500 Subject: [PATCH 02/10] Enhance Reddit filter settings navigation and UI Refactor Reddit filter settings integration in AppSettings and UserDrawer controllers. Add navigation and UI updates for Reddit filter options. --- Settings.x | 117 ++++++++++++++++++++++++----------------------------- 1 file changed, 52 insertions(+), 65 deletions(-) diff --git a/Settings.x b/Settings.x index ef55db9..f695174 100644 --- a/Settings.x +++ b/Settings.x @@ -7,87 +7,32 @@ NSBundle *redditFilterBundle; extern UIImage *iconWithName(NSString *iconName); extern NSString *localizedString(NSString *key, NSString *table); -@interface AppSettingsViewController () -@property(nonatomic, assign) NSInteger feedFilterSectionIndex; -@end - -@interface UserDrawerViewController () -- (void)navigateToRedditFilterSettings; -@end - -%hook AppSettingsViewController -%property(nonatomic, assign) NSInteger feedFilterSectionIndex; -- (void)viewDidLoad { - %orig; - for (int section = 0; section < [self numberOfSectionsInTableView:self.tableView]; section++) { - BaseTableReusableView *headerView = (BaseTableReusableView *)[self tableView:self.tableView - viewForHeaderInSection:section]; - if (!headerView) continue; - BaseLabel *label = headerView.contentView.subviews[0]; - for (NSString *key in @[ @"drawer.settings.feedOptions", @"drawer.settings.viewOptions" ]) { - if ([label.text isEqualToString:[localizedString(key, @"user") uppercaseString]]) { - self.feedFilterSectionIndex = section; - return; - } - } - } - self.feedFilterSectionIndex = 2; -} -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - NSInteger result = %orig; - if (section == self.feedFilterSectionIndex) result++; - return result; -} -- (UITableViewCell *)tableView:(UITableView *)tableView - cellForRowAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section == self.feedFilterSectionIndex && - indexPath.row == [self tableView:tableView numberOfRowsInSection:indexPath.section] - 1) { - UIImage *iconImage = [iconWithName(@"icon_filter") ?: iconWithName(@"icon-filter-outline") - imageScaledToSize:CGSizeMake(20, 20)]; - UIImage *accessoryIconImage = - [iconWithName(@"icon_forward") imageScaledToSize:CGSizeMake(20, 20)]; - ImageLabelTableViewCell *cell = [self dequeueSettingsCellForTableView:tableView - indexPath:indexPath - leadingImage:iconImage - text:@"RedditFilter"]; - [cell setCustomAccessoryImage:accessoryIconImage]; - return cell; - } - return %orig; -} -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section == self.feedFilterSectionIndex && - indexPath.row == [self tableView:tableView numberOfRowsInSection:indexPath.section] - 1) { - [self.navigationController - pushViewController:[(FeedFilterSettingsViewController *)[objc_getClass( - "FeedFilterSettingsViewController") alloc] - initWithStyle:UITableViewStyleGrouped] - animated:YES]; - return; - } - %orig; -} -%end - +// We keep this just in case the side menu still works, as it provides a backup entry point. %hook UserDrawerViewController + - (void)defineAvailableUserActions { %orig; [self.availableUserActions addObject:@1337]; } + - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if ([tableView isEqual:self.actionsTableView] && self.availableUserActions[indexPath.row].unsignedIntegerValue == 1337) { UITableViewCell *cell = [self.actionsTableView dequeueReusableCellWithIdentifier:@"UserDrawerActionTableViewCell"]; + cell.textLabel.text = @"RedditFilter"; - cell.imageView.image = [[iconWithName(@"rpl3/filter") ?: iconWithName(@"icon_filter") ?: iconWithName(@"icon-filter-outline") - imageScaledToSize:CGSizeMake(20, 20)] - imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + + UIImage *icon = iconWithName(@"rpl3/filter") ?: iconWithName(@"icon_filter") ?: iconWithName(@"icon-filter-outline"); + if (icon) { + cell.imageView.image = [[icon imageScaledToSize:CGSizeMake(20, 20)] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + } return cell; } return %orig; } + %new - (void)navigateToRedditFilterSettings { [self dismissViewControllerAnimated:YES completion:nil]; @@ -96,6 +41,7 @@ extern NSString *localizedString(NSString *key, NSString *table); initWithStyle:UITableViewStyleGrouped]; [[self currentNavigationController] pushViewController:filterSettingsViewController animated:YES]; } + - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if ([tableView isEqual:self.actionsTableView] && self.availableUserActions[indexPath.row].unsignedIntegerValue == 1337) { @@ -105,6 +51,47 @@ extern NSString *localizedString(NSString *key, NSString *table); } %end +%hook UIViewController + +- (void)viewDidAppear:(BOOL)animated { + %orig; + + NSString *name = NSStringFromClass([self class]); + + if ([name containsString:@"RedditSliceKit"] && + [name containsString:@"AppSettingsView"] && + [name containsString:@"HostingController"]) { + + if (self.navigationItem.rightBarButtonItems) { + for (UIBarButtonItem *item in self.navigationItem.rightBarButtonItems) { + if (item.tag == 1337) return; + } + } + + UIBarButtonItem *filterButton = [[UIBarButtonItem alloc] initWithTitle:@"Filter" + style:UIBarButtonItemStylePlain + target:self + action:@selector(openRedditFilterFromNav)]; + filterButton.tag = 1337; // Tag to identify our button + + // 5. Add it to the Navigation Bar + NSMutableArray *items = [self.navigationItem.rightBarButtonItems mutableCopy]; + if (!items) items = [NSMutableArray array]; + [items insertObject:filterButton atIndex:0]; // Add to the start of the list (right side) + + self.navigationItem.rightBarButtonItems = items; + } +} + +%new +- (void)openRedditFilterFromNav { + // Launch the Tweak Settings + FeedFilterSettingsViewController *vc = [(FeedFilterSettingsViewController *)[objc_getClass("FeedFilterSettingsViewController") alloc] initWithStyle:UITableViewStyleGrouped]; + [self.navigationController pushViewController:vc animated:YES]; +} + +%end + %ctor { redditFilterBundle = [NSBundle bundleWithPath:[NSBundle.mainBundle pathForResource:@"RedditFilter" ofType:@"bundle"]]; From 6d262ad21877fe62b6db6a9c0ad12522c8b2d698 Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:30:05 -0500 Subject: [PATCH 03/10] Add method declaration for Reddit filter navigation --- Settings.x | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Settings.x b/Settings.x index f695174..2e6e5cf 100644 --- a/Settings.x +++ b/Settings.x @@ -7,6 +7,10 @@ NSBundle *redditFilterBundle; extern UIImage *iconWithName(NSString *iconName); extern NSString *localizedString(NSString *key, NSString *table); +@interface UserDrawerViewController () +- (void)navigateToRedditFilterSettings; +@end + // We keep this just in case the side menu still works, as it provides a backup entry point. %hook UserDrawerViewController From 5a03ec56b77a18c3b8d9dd53652eda4b918d09ac Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:46:58 -0500 Subject: [PATCH 04/10] Rename filter button to 'RedditFilter' and fix alignment --- Settings.x | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Settings.x b/Settings.x index 2e6e5cf..374c33a 100644 --- a/Settings.x +++ b/Settings.x @@ -72,12 +72,14 @@ extern NSString *localizedString(NSString *key, NSString *table); } } - UIBarButtonItem *filterButton = [[UIBarButtonItem alloc] initWithTitle:@"Filter" + UIBarButtonItem *filterButton = [[UIBarButtonItem alloc] initWithTitle:@"RedditFilter" style:UIBarButtonItemStylePlain target:self action:@selector(openRedditFilterFromNav)]; filterButton.tag = 1337; // Tag to identify our button + [filterButton setTitlePositionAdjustment:UIOffsetMake(0, 3.5) forBarMetrics:UIBarMetricsDefault]; + // 5. Add it to the Navigation Bar NSMutableArray *items = [self.navigationItem.rightBarButtonItems mutableCopy]; if (!items) items = [NSMutableArray array]; From 9908d555f3dffaff8139c0a57eba01a82ccdf7aa Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:56:18 -0500 Subject: [PATCH 05/10] Use viewWillAppear for fixing pop-in effect --- Settings.x | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Settings.x b/Settings.x index 374c33a..09836bb 100644 --- a/Settings.x +++ b/Settings.x @@ -57,8 +57,8 @@ extern NSString *localizedString(NSString *key, NSString *table); %hook UIViewController -- (void)viewDidAppear:(BOOL)animated { - %orig; +- (void)viewWillAppear:(BOOL)animated { + %orig; NSString *name = NSStringFromClass([self class]); From 8479f65cccefb74c43e3da869b2d61b997868f79 Mon Sep 17 00:00:00 2001 From: David Wojcik Date: Fri, 13 Feb 2026 12:48:18 -0500 Subject: [PATCH 06/10] Fix: Include preference bundle localizable strings in package - Added after-stage hook to copy bundle resources - Ensures Root.strings file is included in .deb - Fixes blank settings labels issue --- Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8d99c65..e054998 100755 --- a/Makefile +++ b/Makefile @@ -28,4 +28,11 @@ ifeq ($(SIDELOADED),1) include $(THEOS_MAKE_PATH)/aggregate.mk endif -include $(THEOS_MAKE_PATH)/tweak.mk \ No newline at end of file +include $(THEOS_MAKE_PATH)/tweak.mk + +# Copy preference bundle resources to staging directory +after-stage:: + $(ECHO_NOTHING)mkdir -p $(THEOS_STAGING_DIR)/Library/PreferenceBundles/RedditFilter.bundle/en.lproj$(ECHO_END) + $(ECHO_NOTHING)if [ -d "layout/Library/Application Support/RedditFilter.bundle" ]; then \ + cp -r "layout/Library/Application Support/RedditFilter.bundle"/* "$(THEOS_STAGING_DIR)/Library/PreferenceBundles/RedditFilter.bundle/"; \ + fi$(ECHO_END) \ No newline at end of file From dd6576b1c003bab544e25c4deb1e86484acafaaa Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:16:54 -0500 Subject: [PATCH 07/10] Bump package version to 1.1.9 --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e054998..b2bb7dd 100755 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ INSTALL_TARGET_PROCESSES = RedditApp Reddit ARCHS = arm64 -PACKAGE_VERSION = 1.1.8 +PACKAGE_VERSION = 1.1.9 ifdef APP_VERSION PACKAGE_VERSION := $(APP_VERSION)-$(PACKAGE_VERSION) endif @@ -35,4 +35,4 @@ after-stage:: $(ECHO_NOTHING)mkdir -p $(THEOS_STAGING_DIR)/Library/PreferenceBundles/RedditFilter.bundle/en.lproj$(ECHO_END) $(ECHO_NOTHING)if [ -d "layout/Library/Application Support/RedditFilter.bundle" ]; then \ cp -r "layout/Library/Application Support/RedditFilter.bundle"/* "$(THEOS_STAGING_DIR)/Library/PreferenceBundles/RedditFilter.bundle/"; \ - fi$(ECHO_END) \ No newline at end of file + fi$(ECHO_END) From d8edee967d0cbdd806cc0a796526d846f00b4411 Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:18:44 -0500 Subject: [PATCH 08/10] 1.1.9 Schema Paths + optimizations Add Reddit filter preferences and improve filtering logic for posts and comments. --- Tweak.xm | 320 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 206 insertions(+), 114 deletions(-) diff --git a/Tweak.xm b/Tweak.xm index 4e0230c..533d4fb 100755 --- a/Tweak.xm +++ b/Tweak.xm @@ -11,6 +11,16 @@ // --- Cache Setup --- static NSCache *imageCache; static NSCache *stringCache; +static NSSet *ignoredOperationsSet; + +typedef struct { + BOOL promoted; + BOOL recommended; + BOOL nsfw; + BOOL awards; + BOOL scores; + BOOL automod; +} RedditFilterPrefs; @interface CUICatalog : NSObject { NSBundle *_bundle; @@ -126,147 +136,214 @@ static NSArray *filteredObjects(NSArray *objects) { }]]; } -static void filterNode(NSMutableDictionary *node) { - if (![node isKindOfClass:NSMutableDictionary.class]) return; +static void filterNode(NSMutableDictionary *node, RedditFilterPrefs prefs) { + if (![node isKindOfClass:NSMutableDictionary.class]) return; - NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; + // 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; - // Regular post - if ([node[@"__typename"] isEqualToString:@"SubredditPost"]) { - if ([defaults boolForKey:kRedditFilterAwards]) { - node[@"awardings"] = @[]; - node[@"isGildable"] = @NO; - } - if ([defaults boolForKey:kRedditFilterScores]) - node[@"isScoreHidden"] = @YES; - if ([defaults boolForKey:kRedditFilterNSFW] && [node[@"isNsfw"] boolValue]) - node[@"isHidden"] = @YES; - } - - // CellGroup handling - if ([node[@"__typename"] isEqualToString:@"CellGroup"]) { - // Helper to filter cells - NSMutableArray *cells = node[@"cells"]; - if ([cells isKindOfClass:[NSMutableArray class]]) { - for (NSMutableDictionary *cell in cells) { - if (![cell isKindOfClass:NSMutableDictionary.class]) continue; - - if ([cell[@"__typename"] isEqualToString:@"ActionCell"]) { - if ([defaults boolForKey:kRedditFilterAwards]) { - cell[@"isAwardHidden"] = @YES; - id goldenUpvoteInfo = cell[@"goldenUpvoteInfo"]; - if ([goldenUpvoteInfo isKindOfClass:NSDictionary.class] && - ![goldenUpvoteInfo isEqual:[NSNull null]]) { - // Ensure we can mutate it, though usually JSON deserialization with MutableContainers handles this - if ([goldenUpvoteInfo isKindOfClass:NSMutableDictionary.class]) { - ((NSMutableDictionary *)goldenUpvoteInfo)[@"isGildable"] = @NO; - } - } + if ([typeName isEqualToString:@"SubredditPost"]) { + if (prefs.awards) { + node[@"awardings"] = @[]; + node[@"isGildable"] = @NO; + } + if (prefs.scores) node[@"isScoreHidden"] = @YES; + if (prefs.nsfw && [node[@"isNsfw"] boolValue]) node[@"isHidden"] = @YES; + } + else if ([typeName isEqualToString:@"Comment"]) { + if (prefs.awards) { + node[@"awardings"] = @[]; + node[@"isGildable"] = @NO; + } + if (prefs.scores) node[@"isScoreHidden"] = @YES; + + if (prefs.automod) { + NSDictionary *authorInfo = node[@"authorInfo"]; + if ([authorInfo isKindOfClass:NSDictionary.class] && [authorInfo[@"id"] isEqualToString:@"t2_6l4z3"]) { + node[@"isInitiallyCollapsed"] = @YES; } - if ([defaults boolForKey:kRedditFilterScores]) - cell[@"isScoreHidden"] = @YES; - } } } + else if ([typeName isEqualToString:@"CellGroup"]) { + // 1. Check Promoted (AdPayloads) + if (prefs.promoted && [node[@"adPayload"] isKindOfClass:NSDictionary.class]) { + node[@"cells"] = @[]; + return; // Exit early if we cleared the cells + } - // Check for ads in CellGroup - if ([defaults boolForKey:kRedditFilterPromoted] && - [node[@"adPayload"] isKindOfClass:NSDictionary.class]) { - node[@"cells"] = @[]; - } - - // Check for recommendations in CellGroup - if ([defaults boolForKey:kRedditFilterRecommended] && - ![node[@"recommendationContext"] isEqual:[NSNull null]] && - [node[@"recommendationContext"] isKindOfClass:NSDictionary.class]) { - NSDictionary *recommendationContext = node[@"recommendationContext"]; - id typeName = recommendationContext[@"typeName"]; - id typeIdentifier = recommendationContext[@"typeIdentifier"]; - id isContextHidden = recommendationContext[@"isContextHidden"]; - if (![typeIdentifier isEqual:[NSNull null]] && ![typeName isEqual:[NSNull null]] && - ![isContextHidden isEqual:[NSNull null]] && - [typeIdentifier isKindOfClass:NSString.class] && - [typeName isKindOfClass:NSString.class] && - [isContextHidden isKindOfClass:NSNumber.class]) { - if (!(([typeName isEqualToString:@"PopularRecommendationContext"] || - [typeIdentifier hasPrefix:@"global_popular"]) && - [isContextHidden boolValue])) { - node[@"cells"] = @[]; + // 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 + } + } + } + + // 3. Process remaining ActionCells ONLY if Awards or Scores filters are enabled + if (prefs.awards || prefs.scores) { + NSMutableArray *cells = node[@"cells"]; + 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; + id goldenInfo = cell[@"goldenUpvoteInfo"]; + if ([goldenInfo isKindOfClass:NSMutableDictionary.class]) { + ((NSMutableDictionary *)goldenInfo)[@"isGildable"] = @NO; + } + } + if (prefs.scores) cell[@"isScoreHidden"] = @YES; + } + } + } } - } } - } - // Ad post - if ([defaults boolForKey:kRedditFilterPromoted]) { - if ([node[@"__typename"] isEqualToString:@"AdPost"]) { - node[@"isHidden"] = @YES; + else if ([typeName isEqualToString:@"AdPost"]) { + if (prefs.promoted) node[@"isHidden"] = @YES; } - } - // Comment - if ([node[@"__typename"] isEqualToString:@"Comment"]) { - if ([defaults boolForKey:kRedditFilterAwards]) { - node[@"awardings"] = @[]; - node[@"isGildable"] = @NO; - } - if ([defaults boolForKey:kRedditFilterScores]) - node[@"isScoreHidden"] = @YES; - if ([node[@"authorInfo"] isKindOfClass:NSDictionary.class] && - [node[@"authorInfo"][@"id"] isEqualToString:@"t2_6l4z3"] && - [defaults boolForKey:kRedditFilterAutoCollapseAutoMod]) - node[@"isInitiallyCollapsed"] = @YES; - } } %hook NSURLSession - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler { - if (![request.URL.host hasPrefix:@"gql"] && ![request.URL.host hasPrefix:@"oauth"]) + if (![request.URL.host hasPrefix:@"gql"] && + ![request.URL.host hasPrefix:@"oauth"]) return %orig; - + void (^newCompletionHandler)(NSData *, NSURLResponse *, NSError *) = ^(NSData *data, NSURLResponse *response, NSError *error) { if (error || !data) 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; - - if (json[@"data"] && [json[@"data"] isKindOfClass:NSDictionary.class]) { - NSDictionary *dataDict = json[@"data"]; - NSMutableDictionary *root = dataDict.allValues.firstObject; - - if ([root isKindOfClass:NSDictionary.class]) { - if ([root.allValues.firstObject isKindOfClass:NSDictionary.class] && - root.allValues.firstObject[@"edges"]) - for (NSMutableDictionary *edge in root.allValues.firstObject[@"edges"]) - filterNode(edge[@"node"]); - - if (root[@"commentForest"]) - for (NSMutableDictionary *tree in root[@"commentForest"][@"trees"]) - filterNode(tree[@"node"]); + + // 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 + 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"]; + } else if ([request.URL.query containsString:@"operationName="]) { + NSArray *components = [request.URL.query componentsSeparatedByString:@"&"]; + for (NSString *param in components) { + if ([param hasPrefix:@"operationName="]) { + operationName = [param substringFromIndex:14]; + break; + } + } + } + + // Ignore Telemetry & Configs (Performance Saver) + if ([ignoredOperationsSet containsObject:operationName]) { + return completionHandler(data, response, error); + } + + // Fast Path based on known schemas + if ([operationName isEqualToString:@"HomeFeedSdui"]) { + if ([json valueForKeyPath:@"data.homeV3.elements.edges"]) { + for (NSMutableDictionary *edge in json[@"data"][@"homeV3"][@"elements"][@"edges"]) { + filterNode(edge[@"node"], prefs); + } + } + } else if ([operationName isEqualToString:@"PopularFeedSdui"]) { + if ([json valueForKeyPath:@"data.popularV3.elements.edges"]) { + for (NSMutableDictionary *edge in json[@"data"][@"popularV3"][@"elements"][@"edges"]) { + filterNode(edge[@"node"], prefs); + } + } + } else if ([operationName isEqualToString:@"FeedPostDetailsByIds"]) { + if ([json valueForKeyPath:@"data.postsInfoByIds"]) { + for (NSMutableDictionary *node in json[@"data"][@"postsInfoByIds"]) { + filterNode(node, prefs); + } + } + } else if ([operationName isEqualToString:@"PostInfoByIdComments"] || [operationName isEqualToString:@"PostInfoById"]) { + if ([json valueForKeyPath:@"data.postInfoById.commentForest.trees"]) { + for (NSMutableDictionary *tree in json[@"data"][@"postInfoById"][@"commentForest"][@"trees"]) { + filterNode(tree[@"node"], prefs); + } + } + if ([json valueForKeyPath:@"data.postInfoById"]) { + filterNode(json[@"data"][@"postInfoById"], prefs); + } + } else if ([operationName isEqualToString:@"PdpCommentsAds"]) { + // Instantly clear out Comment Ads + if ([NSUserDefaults.standardUserDefaults boolForKey:kRedditFilterPromoted]) { + if (json[@"data"] && [json[@"data"] isKindOfClass:NSDictionary.class]) { + NSMutableDictionary *dataDict = json[@"data"]; + if (dataDict.allValues.firstObject[@"pdpCommentsAds"]) { + dataDict.allValues.firstObject[@"pdpCommentsAds"] = @[]; + } + } + } + } else { + // Original recursive logic for unknown queries (like ProfileFeedSdui) + if (json[@"data"] && [json[@"data"] isKindOfClass:NSDictionary.class]) { + NSDictionary *dataDict = json[@"data"]; + NSMutableDictionary *root = dataDict.allValues.firstObject; + + if ([root isKindOfClass:NSDictionary.class]) { + if ([root.allValues.firstObject isKindOfClass:NSDictionary.class] && + root.allValues.firstObject[@"edges"]) + for (NSMutableDictionary *edge in root.allValues.firstObject[@"edges"]) + filterNode(edge[@"node"], prefs); + + if (root[@"commentForest"]) + for (NSMutableDictionary *tree in root[@"commentForest"][@"trees"]) + filterNode(tree[@"node"], prefs); + + NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; + BOOL filterPromoted = [defaults boolForKey:kRedditFilterPromoted]; - NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; - BOOL filterPromoted = [defaults boolForKey:kRedditFilterPromoted]; - - if (root[@"commentsPageAds"] && filterPromoted) - root[@"commentsPageAds"] = @[]; - if (root[@"commentTreeAds"] && filterPromoted) - root[@"commentTreeAds"] = @[]; - if (root[@"pdpCommentsAds"] && filterPromoted) - root[@"pdpCommentsAds"] = @[]; - if (root[@"recommendations"] && [defaults boolForKey:kRedditFilterRecommended]) - root[@"recommendations"] = @[]; - - } else if ([root isKindOfClass:NSArray.class]) { - for (NSMutableDictionary *node in (NSArray *)root) filterNode(node); + if (root[@"commentsPageAds"] && filterPromoted) + root[@"commentsPageAds"] = @[]; + + if (root[@"commentTreeAds"] && filterPromoted) + root[@"commentTreeAds"] = @[]; + + if (root[@"pdpCommentsAds"] && filterPromoted) // Kept just in case the fast path misses + root[@"pdpCommentsAds"] = @[]; + + if (root[@"recommendations"] && [defaults boolForKey:kRedditFilterRecommended]) + root[@"recommendations"] = @[]; + } else if ([root isKindOfClass:NSArray.class]) { + for (NSMutableDictionary *node in (NSArray *)root) filterNode(node, prefs); + } } } @@ -363,7 +440,7 @@ static void filterNode(NSMutableDictionary *node) { - (void)updateConstraints { %orig; - // Fix: Prevent adding duplicate constraints if updateConstraints is called multiple times. + // Prevent adding duplicate constraints if updateConstraints is called multiple times. // Use an associated object to track if we've already done this. NSNumber *constraintsAdded = objc_getAssociatedObject(self, @selector(updateConstraints)); if (constraintsAdded.boolValue) return; @@ -417,6 +494,21 @@ static void filterNode(NSMutableDictionary *node) { imageCache = [[NSCache alloc] init]; stringCache = [[NSCache alloc] init]; + // Initialize Ignored Operations Set + ignoredOperationsSet = [[NSSet alloc] initWithObjects: + @"GetAccount", @"FetchIdentityPreferences", @"DynamicConfigsByNames", + @"GetAllExperimentVariants", @"AdsOffRedditLocation", @"UserLocation", + @"CookiePreferences", @"FetchSubscribedSubreddits", @"AdsOffRedditPreferences", + @"Age", @"RecommendedPrompts", @"EnrollInGamification", @"BadgeCounts", + @"GetEligibleUXExperiences", @"GetUserAdEligibility", @"GoldBalances", + @"PaymentSubscriptions", @"FeaturedDevvitGame", @"ModQueueNewItemCount", + @"LastModeratedSubredditName", @"AwardProductOffers", @"BlockedRedditors", + @"GamesPreferences", @"GetRedditUsersByIds", @"SubredditsForNames", + @"SubredditsForIds", @"ExposeExperimentBatch", @"GetProfilePostFlairTemplates", + @"GetRedditorByNameApollo", @"GetActiveSubreddits", @"GetMyShowcaseCarousel", + @"UserPublicTrophies", @"PostDraftsCount", @"BrandToolsStatus", + @"NotificationInbox", @"TrendingSearchesQuery", nil]; + assetBundles = [NSMutableArray array]; assetCatalogs = [NSMutableArray array]; [assetBundles addObject:NSBundle.mainBundle]; @@ -448,7 +540,7 @@ static void filterNode(NSMutableDictionary *node) { if (!error) [assetCatalogs addObject:catalog]; } - // Fix: Correct keys used for default values. Previously all checks were for kRedditFilterPromoted. + // Correct keys used for default values. Previously all checks were for kRedditFilterPromoted. NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; if (![defaults objectForKey:kRedditFilterPromoted]) From 37450dbc98d2533ccbcb979579a8e97e4b3b15b6 Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:50:04 -0500 Subject: [PATCH 09/10] 1.20 --- Tweak.xm | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Tweak.xm b/Tweak.xm index 533d4fb..d94ffe6 100755 --- a/Tweak.xm +++ b/Tweak.xm @@ -45,8 +45,15 @@ extern "C" UIImage *iconWithName(NSString *iconName) { if ([imageName hasPrefix:iconName] && (imageName.length == iconName.length || imageName.length == iconName.length + 3)) { + // SAFELY retrieve the private _bundle ivar + Ivar bundleIvar = class_getInstanceVariable(object_getClass(catalog), "_bundle"); + if (!bundleIvar) continue; + + NSBundle *bundle = object_getIvar(catalog, bundleIvar); + if (!bundle) continue; + UIImage *image = [UIImage imageNamed:imageName - inBundle:object_getIvar(catalog, class_getInstanceVariable(object_getClass(catalog), "_bundle")) + inBundle:bundle compatibleWithTraitCollection:nil]; if (image) { @@ -436,13 +443,15 @@ static void filterNode(NSMutableDictionary *node, RedditFilterPrefs prefs) { } %end +// Create a static key for associated objects +static char kConstraintsAddedKey; + %hook ToggleImageTableViewCell - (void)updateConstraints { %orig; // Prevent adding duplicate constraints if updateConstraints is called multiple times. - // Use an associated object to track if we've already done this. - NSNumber *constraintsAdded = objc_getAssociatedObject(self, @selector(updateConstraints)); + NSNumber *constraintsAdded = objc_getAssociatedObject(self, &kConstraintsAddedKey); if (constraintsAdded.boolValue) return; UIStackView *horizontalStackView = [self respondsToSelector:@selector(imageLabelView)] @@ -482,7 +491,7 @@ static void filterNode(NSMutableDictionary *node, RedditFilterPrefs prefs) { ]]; // Mark as added - objc_setAssociatedObject(self, @selector(updateConstraints), @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + objc_setAssociatedObject(self, &kConstraintsAddedKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } %end From a16409c3398acde7ebd3aaf5cd034c6a52562f9c Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:54:40 -0500 Subject: [PATCH 10/10] Bump package version to 1.2.0 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b2bb7dd..6a14a4f 100755 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ INSTALL_TARGET_PROCESSES = RedditApp Reddit ARCHS = arm64 -PACKAGE_VERSION = 1.1.9 +PACKAGE_VERSION = 1.2.0 ifdef APP_VERSION PACKAGE_VERSION := $(APP_VERSION)-$(PACKAGE_VERSION) endif