Skip to content

Commit ea36609

Browse files
committed
Merge branch 'ios-classifiers'
* ios-classifiers: Bump version to 14.1 (319) Fix iPad/Catalyst story detail mismatch when server stories replace cache Fix Catalyst trainer regex popover, input styling, and Ask AI popover height Theme-aware story list separator colors and bump version to 14.1 (318) Move feed list toolbar buttons inward to avoid iPhone rounded corners Replace jQuery mark.js with pure JS version for text classifier highlighting Add text/URL/regex classifiers, scope support, and modernize sheet presentations
2 parents 71608c9 + df38e50 commit ea36609

27 files changed

+3014
-198
lines changed

clients/ios/Classes/AddSiteViewController.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
@property (nonatomic) IBOutlet UITextField *inFolderInput;
2626
@property (nonatomic) IBOutlet UITextField *addFolderInput;
2727
@property (nonatomic) IBOutlet UITextField *siteAddressInput;
28+
@property (nonatomic) IBOutlet UIButton *addSiteButton;
2829

2930
@property (nonatomic) IBOutlet UIBarButtonItem *addButton;
3031
@property (nonatomic) IBOutlet UIBarButtonItem *cancelButton;

clients/ios/Classes/AddSiteViewController.m

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
3131

3232
- (void)viewDidLoad {
3333
appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
34-
35-
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(doCancelButton)];
36-
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Add Site" style:UIBarButtonItemStyleDone target:self action:@selector(addSite)];
34+
self.navigationItem.leftBarButtonItem = nil;
35+
self.navigationItem.rightBarButtonItem = nil;
3736

3837
UIView *folderPadding = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 24, 16)];
3938
UIImageView *folderImage = [[UIImageView alloc]
@@ -82,6 +81,9 @@ - (void)viewWillAppear:(BOOL)animated {
8281
[self.addingLabel setHidden:YES];
8382
[self.activityIndicator stopAnimating];
8483

84+
if (self.navigationController) {
85+
self.navigationController.navigationBarHidden = YES;
86+
}
8587
self.view.backgroundColor = UIColorFromRGB(NEWSBLUR_WHITE_COLOR);
8688
self.siteTable.backgroundColor = UIColorFromRGB(NEWSBLUR_WHITE_COLOR);
8789
// eliminate extra separators at bottom of site table (e.g., while animating)
@@ -129,11 +131,7 @@ - (CGSize)preferredContentSize {
129131
}
130132

131133
- (IBAction)doCancelButton {
132-
if (!self.isPhone) {
133-
[appDelegate hidePopover];
134-
} else {
135-
[appDelegate hidePopoverAnimated:YES];
136-
}
134+
[self dismissAddSiteDialog];
137135
}
138136

139137
- (IBAction)doAddButton {
@@ -271,11 +269,7 @@ - (IBAction)addSite {
271269
[self.errorLabel setText:[responseObject valueForKey:@"message"]];
272270
[self.errorLabel setHidden:NO];
273271
} else {
274-
if (!self.isPhone) {
275-
[self->appDelegate hidePopover];
276-
} else {
277-
[self->appDelegate hidePopoverAnimated:YES];
278-
}
272+
[self dismissAddSiteDialog];
279273
[self->appDelegate reloadFeedsView:NO];
280274
}
281275

@@ -292,6 +286,20 @@ - (IBAction)addSite {
292286
}];
293287
}
294288

289+
- (void)dismissAddSiteDialog {
290+
UIViewController *presenter = self.navigationController ?: self;
291+
if (presenter.presentingViewController) {
292+
[presenter dismissViewControllerAnimated:YES completion:nil];
293+
return;
294+
}
295+
296+
if (!self.isPhone) {
297+
[appDelegate hidePopover];
298+
} else {
299+
[appDelegate hidePopoverAnimated:YES];
300+
}
301+
}
302+
295303
- (NSString *)extractParentFolder {
296304
NSString *parent_folder = [self.inFolderInput text];
297305
NSInteger folder_loc = [parent_folder rangeOfString:@"" options:NSBackwardsSearch].location;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// ClassifierScope.swift
3+
// NewsBlur
4+
//
5+
// Created by Samuel Clay on 2026-03-03.
6+
// Copyright © 2026 NewsBlur. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
/// The scope at which a classifier operates: per-feed, per-folder, or globally.
12+
enum ClassifierScope: String, CaseIterable {
13+
case feed = "feed"
14+
case folder = "folder"
15+
case global = "global"
16+
17+
var iconName: String {
18+
switch self {
19+
case .feed: return "dot.radiowaves.left.and.right"
20+
case .folder: return "folder"
21+
case .global: return "globe"
22+
}
23+
}
24+
25+
/// Color when the scope icon is active on a neutral (lighter gray) capsule.
26+
var activeColor: Color {
27+
switch self {
28+
case .feed: return Color(white: 0.35)
29+
case .folder: return Color(red: 0.231, green: 0.510, blue: 0.965) // #3B82F6
30+
case .global: return Color(red: 0.545, green: 0.361, blue: 0.965) // #8B5CF6
31+
}
32+
}
33+
34+
/// Lighter color for active scope icon on a like/dislike (green/red) capsule.
35+
var activeLightColor: Color {
36+
switch self {
37+
case .feed: return Color(white: 0.9)
38+
case .folder: return Color(red: 0.576, green: 0.773, blue: 0.992) // #93C5FD
39+
case .global: return Color(red: 0.769, green: 0.710, blue: 0.992) // #C4B5FD
40+
}
41+
}
42+
43+
/// Dark mode active color.
44+
var activeDarkColor: Color {
45+
switch self {
46+
case .feed: return Color(white: 0.67) // #AAA
47+
case .folder: return Color(red: 0.376, green: 0.647, blue: 0.980) // #60A5FA
48+
case .global: return Color(red: 0.655, green: 0.545, blue: 0.980) // #A78BFA
49+
}
50+
}
51+
52+
/// Returns a label like "Feed Title", "Folder Text", "Global Author".
53+
func label(for classifierType: String) -> String {
54+
switch self {
55+
case .feed: return "Feed \(classifierType)"
56+
case .folder: return "Folder \(classifierType)"
57+
case .global: return "Global \(classifierType)"
58+
}
59+
}
60+
}

clients/ios/Classes/Feed.swift

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,20 @@ typealias AnyDictionary = [AnyHashable : Any]
7171
let name: String
7272
let count: Int
7373
let score: Score
74-
74+
var scope: ClassifierScope = .feed
75+
var folderName: String = ""
76+
7577
var id: String {
7678
return name
7779
}
7880
}
7981

8082
lazy var titles: [Training] = {
81-
guard let appDelegate = NewsBlurAppDelegate.shared,
82-
let classifierTitles = self.classifiers(for: "titles") else {
83-
return []
84-
}
85-
86-
let userTitles = classifierTitles.map { Training(name: $0.key as! String, count: 0, score: Score(rawValue: $0.value as? Int ?? 0) ?? .none) }
87-
88-
return userTitles.sorted()
83+
return trainings(for: "titles")
84+
}()
85+
86+
lazy var titleRegex: [Training] = {
87+
return trainings(for: "title_regex")
8988
}()
9089

9190
lazy var authors: [Training] = {
@@ -127,6 +126,22 @@ typealias AnyDictionary = [AnyHashable : Any]
127126

128127
return userTags.sorted() + otherTags
129128
}()
129+
130+
lazy var texts: [Training] = {
131+
return trainings(for: "texts")
132+
}()
133+
134+
lazy var textRegex: [Training] = {
135+
return trainings(for: "text_regex")
136+
}()
137+
138+
lazy var urls: [Training] = {
139+
return trainings(for: "urls")
140+
}()
141+
142+
lazy var urlRegex: [Training] = {
143+
return trainings(for: "url_regex")
144+
}()
130145

131146
init(id: String) {
132147
self.id = id
@@ -171,6 +186,20 @@ typealias AnyDictionary = [AnyHashable : Any]
171186

172187
isRiverOrSocial = storiesCollection.isRiverOrSocial
173188
}
189+
190+
private func trainings(for key: String) -> [Training] {
191+
guard let classifiers = self.classifiers(for: key) else {
192+
return []
193+
}
194+
195+
let userItems = classifiers.map {
196+
Training(name: $0.key as! String,
197+
count: 0,
198+
score: Score(rawValue: $0.value as? Int ?? 0) ?? .none)
199+
}
200+
201+
return userItems.sorted()
202+
}
174203

175204
func color(for key: String, from feed: AnyDictionary, default defaultHex: String) -> UIColor {
176205
let hex = feed[key] as? String ?? defaultHex

clients/ios/Classes/FeedChooserViewController.m

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,35 @@ - (void)viewDidLoad {
122122
}
123123
}
124124

125+
- (void)viewDidAppear:(BOOL)animated {
126+
[super viewDidAppear:animated];
127+
}
128+
129+
- (void)viewWillAppear:(BOOL)animated {
130+
[super viewWillAppear:animated];
131+
[self updateNavigationBarAppearance];
132+
}
133+
134+
- (void)updateNavigationBarAppearance {
135+
UINavigationBar *navBar = self.navigationController.navigationBar;
136+
navBar.translucent = NO;
137+
[[ThemeManager themeManager] updateNavigationController:self.navigationController];
138+
139+
if (@available(iOS 13.0, *)) {
140+
UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];
141+
[appearance configureWithOpaqueBackground];
142+
UIColor *backgroundColor = UIColorFromLightSepiaMediumDarkRGB(0xE3E6E0, 0xF3E2CB, 0x333333, 0x222222);
143+
appearance.backgroundColor = backgroundColor;
144+
appearance.shadowColor = [UIColor clearColor];
145+
appearance.titleTextAttributes = [UINavigationBar appearance].titleTextAttributes ?: @{};
146+
147+
navBar.standardAppearance = appearance;
148+
navBar.scrollEdgeAppearance = appearance;
149+
navBar.compactAppearance = appearance;
150+
navBar.compactScrollEdgeAppearance = appearance;
151+
}
152+
}
153+
125154
- (void)performGetInactiveFeeds {
126155
[MBProgressHUD hideHUDForView:self.view animated:YES];
127156
MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES];

clients/ios/Classes/FeedDetailObjCViewController.m

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ - (void)viewDidLoad {
109109
self.dashboardSingleMode = NO;
110110

111111
self.storyTitlesTable.backgroundColor = UIColorFromRGB(0xf4f4f4);
112-
self.storyTitlesTable.separatorColor = UIColorFromRGB(0xE9E8E4);
112+
self.storyTitlesTable.separatorColor = UIColorFromLightSepiaMediumDarkRGB(0xE9E8E4, 0xF2E9DE, 0x383838, 0x222222);
113113
if (@available(iOS 15.0, *)) {
114114
self.storyTitlesTable.allowsFocus = NO;
115115
}
@@ -1793,8 +1793,38 @@ - (void)finishedLoadingFeed:(NSDictionary *)results feedPage:(NSInteger)feedPage
17931793
}
17941794

17951795
if (!self.isPhoneOrCompact) {
1796-
[appDelegate.storyPagesViewController resizeScrollView];
1797-
[appDelegate.storyPagesViewController setStoryFromScroll:YES];
1796+
NSInteger pageIndex = appDelegate.storyPagesViewController.currentPage.pageIndex;
1797+
BOOL storyChanged = NO;
1798+
1799+
// Check if the story at the current page position has changed after the refresh.
1800+
// This catches all mismatch cases: story moved, story replaced, story gone.
1801+
if (pageIndex >= 0 && pageIndex < storiesCollection.storyLocationsCount && appDelegate.activeStory) {
1802+
NSInteger storyIndex = [storiesCollection indexFromLocation:pageIndex];
1803+
if (storyIndex >= 0) {
1804+
NSDictionary *storyAtPage = [storiesCollection.activeFeedStories objectAtIndex:storyIndex];
1805+
storyChanged = ![[appDelegate.activeStory objectForKey:@"story_hash"]
1806+
isEqualToString:[storyAtPage objectForKey:@"story_hash"]];
1807+
}
1808+
} else if (appDelegate.activeStory && storiesCollection.storyLocationsCount > 0) {
1809+
// Page index out of range after refresh (fewer stories or initial state)
1810+
storyChanged = YES;
1811+
}
1812+
1813+
if (storyChanged && storiesCollection.storyLocationsCount > 0) {
1814+
// Server data changed the story list. Select the first story and
1815+
// reset pages so the detail pane stays in sync.
1816+
NSInteger firstIndex = [storiesCollection indexFromLocation:0];
1817+
if (firstIndex >= 0) {
1818+
appDelegate.activeStory = [storiesCollection.activeFeedStories objectAtIndex:firstIndex];
1819+
}
1820+
appDelegate.storyPagesViewController.currentPage.pageIndex = -2;
1821+
appDelegate.storyPagesViewController.nextPage.pageIndex = -2;
1822+
appDelegate.storyPagesViewController.previousPage.pageIndex = -2;
1823+
[appDelegate.storyPagesViewController changePage:0 animated:NO];
1824+
} else {
1825+
[appDelegate.storyPagesViewController resizeScrollView];
1826+
[appDelegate.storyPagesViewController setStoryFromScroll:YES];
1827+
}
17981828
}
17991829
[appDelegate.storyPagesViewController advanceToNextUnread];
18001830

@@ -4455,7 +4485,7 @@ - (void)updateTheme {
44554485

44564486
self.view.backgroundColor = UIColorFromRGB(0xf4f4f4);
44574487
self.storyTitlesTable.backgroundColor = UIColorFromRGB(0xf4f4f4);
4458-
self.storyTitlesTable.separatorColor = UIColorFromRGB(0xE9E8E4);
4488+
self.storyTitlesTable.separatorColor = UIColorFromLightSepiaMediumDarkRGB(0xE9E8E4, 0xF2E9DE, 0x383838, 0x222222);
44594489
if (@available(iOS 13.0, *)) {
44604490
self.storyTitlesTable.overrideUserInterfaceStyle = ThemeManager.shared.isDarkTheme ? UIUserInterfaceStyleDark : UIUserInterfaceStyleLight;
44614491
}

clients/ios/Classes/FeedsObjCViewController.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ UIGestureRecognizerDelegate, UITextFieldDelegate> {
5555
@property (nonatomic) IBOutlet NSLayoutConstraint *feedTitlesTopConstraint;
5656
@property (nonatomic) IBOutlet NSLayoutConstraint *feedTitlesLeadingConstraint;
5757
@property (nonatomic) IBOutlet NSLayoutConstraint *feedTitlesTrailingConstraint;
58+
@property (nonatomic) IBOutlet NSLayoutConstraint *toolbarLeadingConstraint;
59+
@property (nonatomic) IBOutlet NSLayoutConstraint *toolbarTrailingConstraint;
5860
@property (nonatomic) IBOutlet NSLayoutConstraint *toolbarBottomConstraint;
5961
@property (nonatomic) IBOutlet UIToolbar *feedViewToolbar;
6062
@property (nonatomic) IBOutlet UISlider * feedScoreSlider;

clients/ios/Classes/FeedsObjCViewController.m

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ @implementation FeedsObjCViewController
8181
@synthesize currentSection;
8282
@synthesize noFocusMessage;
8383
@synthesize noFocusLabel;
84+
@synthesize toolbarLeadingConstraint;
85+
@synthesize toolbarTrailingConstraint;
8486
@synthesize toolbarLeftMargin;
8587
@synthesize updatedDictFeeds_;
8688
@synthesize updatedDictSocialFeeds_;
@@ -314,13 +316,6 @@ - (void)viewDidLoad {
314316
}
315317

316318
- (void)configureFeedToolbarItemsForOrientation:(UIInterfaceOrientation)orientation {
317-
if (appDelegate.isPhone && !UIInterfaceOrientationIsLandscape(orientation)) {
318-
if (self.defaultFeedToolbarItems.count > 0) {
319-
self.feedViewToolbar.items = self.defaultFeedToolbarItems;
320-
}
321-
return;
322-
}
323-
324319
UIBarButtonItem *intelligenceItem = nil;
325320
for (UIBarButtonItem *item in self.feedViewToolbar.items) {
326321
if (item.customView == self.intelligenceControl) {
@@ -345,6 +340,7 @@ - (void)configureFeedToolbarItemsForOrientation:(UIInterfaceOrientation)orientat
345340
space.width = width;
346341
return space;
347342
};
343+
CGFloat compactToolbarSpacing = self.appDelegate.detailViewController.isPhoneOrCompact ? 0.0 : 8.0;
348344

349345
#if TARGET_OS_MACCATALYST
350346
self.feedViewToolbar.items = @[
@@ -358,14 +354,12 @@ - (void)configureFeedToolbarItemsForOrientation:(UIInterfaceOrientation)orientat
358354
];
359355
#else
360356
self.feedViewToolbar.items = @[
361-
makeFlexSpace(),
362357
makeFlexSpace(),
363358
self.addBarButton,
364-
makeFlexSpace(),
359+
makeFixedSpace(compactToolbarSpacing),
365360
intelligenceItem,
366-
makeFlexSpace(),
361+
makeFixedSpace(compactToolbarSpacing),
367362
self.settingsBarButton,
368-
makeFlexSpace(),
369363
makeFlexSpace()
370364
];
371365
#endif
@@ -463,6 +457,9 @@ - (void)viewDidLayoutSubviews {
463457
// Non-Face ID devices (iPhone SE): 8pt to match side margins
464458
CGFloat safeAreaBottom = self.view.safeAreaInsets.bottom;
465459
CGFloat toolbarBottomGap = (safeAreaBottom > 0) ? 12.0 : 8.0;
460+
CGFloat compactToolbarSideInset = self.appDelegate.detailViewController.isPhoneOrCompact ? 8.0 : 0.0;
461+
self.toolbarLeadingConstraint.constant = compactToolbarSideInset;
462+
self.toolbarTrailingConstraint.constant = compactToolbarSideInset;
466463
self.toolbarBottomConstraint.constant = -toolbarBottomGap;
467464
CGFloat toolbarHeight = CGRectGetHeight(self.feedViewToolbar.frame);
468465
CGFloat totalBottomInset = MAX(toolbarHeight + toolbarBottomGap, safeAreaBottom);

clients/ios/Classes/FontSettingsViewController.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,8 @@ - (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NS
427427

428428
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
429429
NSInteger adjustedRow = [self adjustedRow:indexPath.row];
430-
if (adjustedRow != 6) {
431-
[self dismissViewControllerAnimated:adjustedRow != 3 && adjustedRow != 4 && adjustedRow != 5 completion:nil];
430+
if (adjustedRow != 6 && adjustedRow != 3) {
431+
[self dismissViewControllerAnimated:adjustedRow != 4 && adjustedRow != 5 completion:nil];
432432
}
433433

434434
if (adjustedRow == 0) {

0 commit comments

Comments
 (0)