Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 32 additions & 32 deletions hackertracker.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,6 @@
5D9B8ECB2C5345E9009AAA61 /* FeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D9B8ECA2C5345E9009AAA61 /* FeedbackFormView.swift */; };
5D9B8ED22C656FB7009AAA61 /* rubber_5.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 5D9B8ECD2C656FB7009AAA61 /* rubber_5.mp3 */; };
5D9B8ED32C656FB7009AAA61 /* rubber_1.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 5D9B8ECE2C656FB7009AAA61 /* rubber_1.mp3 */; };
5DF0F70200000000F0F0F702 /* JetBrainsMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5DF0F70100000000F0F0F701 /* JetBrainsMono-Regular.ttf */; };
5DF0F70400000000F0F0F704 /* JetBrainsMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5DF0F70300000000F0F0F703 /* JetBrainsMono-Bold.ttf */; };
5DF0F70600000000F0F0F706 /* MajorMonoDisplay-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5DF0F70500000000F0F0F705 /* MajorMonoDisplay-Regular.ttf */; };
5DF0F70800000000F0F0F708 /* JetBrainsMono-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5DF0F70700000000F0F0F707 /* JetBrainsMono-OFL.txt */; };
5DF0F70A00000000F0F0F70A /* MajorMonoDisplay-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5DF0F70900000000F0F0F709 /* MajorMonoDisplay-OFL.txt */; };
5D9B8ED42C656FB7009AAA61 /* rubber_4.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 5D9B8ECF2C656FB7009AAA61 /* rubber_4.mp3 */; };
5D9B8ED52C656FB7009AAA61 /* rubber_2.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 5D9B8ED02C656FB7009AAA61 /* rubber_2.mp3 */; };
5D9B8ED62C656FB7009AAA61 /* rubber_3.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 5D9B8ED12C656FB7009AAA61 /* rubber_3.mp3 */; };
Expand Down Expand Up @@ -115,6 +110,11 @@
5DDF035C2C2767750087BA59 /* ContentCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDF035B2C2767750087BA59 /* ContentCellView.swift */; };
5DDF035E2C28B4F10087BA59 /* ContentDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDF035D2C28B4F10087BA59 /* ContentDetailView.swift */; };
5DE62D952E399B090019C7C3 /* ShareBookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE62D942E399A3E0019C7C3 /* ShareBookmarksView.swift */; };
5DF0F70200000000F0F0F702 /* JetBrainsMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5DF0F70100000000F0F0F701 /* JetBrainsMono-Regular.ttf */; };
5DF0F70400000000F0F0F704 /* JetBrainsMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5DF0F70300000000F0F0F703 /* JetBrainsMono-Bold.ttf */; };
5DF0F70600000000F0F0F706 /* MajorMonoDisplay-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5DF0F70500000000F0F0F705 /* MajorMonoDisplay-Regular.ttf */; };
5DF0F70800000000F0F0F708 /* JetBrainsMono-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5DF0F70700000000F0F0F707 /* JetBrainsMono-OFL.txt */; };
5DF0F70A00000000F0F0F70A /* MajorMonoDisplay-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5DF0F70900000000F0F0F709 /* MajorMonoDisplay-OFL.txt */; };
5DF51AB8285007EC007BCB83 /* DateFormatterUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF51AB7285007EC007BCB83 /* DateFormatterUtility.swift */; };
5DF51ABA2850310B007BCB83 /* ConferenceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF51AB92850310B007BCB83 /* ConferenceRow.swift */; };
5DF51ABC2851203F007BCB83 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF51ABB2851203F007BCB83 /* Document.swift */; };
Expand Down Expand Up @@ -194,11 +194,6 @@
5D9B8ECF2C656FB7009AAA61 /* rubber_4.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = rubber_4.mp3; sourceTree = "<group>"; };
5D9B8ED02C656FB7009AAA61 /* rubber_2.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = rubber_2.mp3; sourceTree = "<group>"; };
5D9B8ED12C656FB7009AAA61 /* rubber_3.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = rubber_3.mp3; sourceTree = "<group>"; };
5DF0F70100000000F0F0F701 /* JetBrainsMono-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "JetBrainsMono-Regular.ttf"; sourceTree = "<group>"; };
5DF0F70300000000F0F0F703 /* JetBrainsMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "JetBrainsMono-Bold.ttf"; sourceTree = "<group>"; };
5DF0F70500000000F0F0F705 /* MajorMonoDisplay-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "MajorMonoDisplay-Regular.ttf"; sourceTree = "<group>"; };
5DF0F70700000000F0F0F707 /* JetBrainsMono-OFL.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "JetBrainsMono-OFL.txt"; sourceTree = "<group>"; };
5DF0F70900000000F0F0F709 /* MajorMonoDisplay-OFL.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "MajorMonoDisplay-OFL.txt"; sourceTree = "<group>"; };
5D9E57442857B398001374D4 /* ConferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConferencesView.swift; sourceTree = "<group>"; };
5D9E57482859037C001374D4 /* BookmarkUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkUtility.swift; sourceTree = "<group>"; };
5D9E5752285A3C25001374D4 /* SpeakersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeakersView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -250,6 +245,11 @@
5DDF035B2C2767750087BA59 /* ContentCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentCellView.swift; sourceTree = "<group>"; };
5DDF035D2C28B4F10087BA59 /* ContentDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentDetailView.swift; sourceTree = "<group>"; };
5DE62D942E399A3E0019C7C3 /* ShareBookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareBookmarksView.swift; sourceTree = "<group>"; };
5DF0F70100000000F0F0F701 /* JetBrainsMono-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "JetBrainsMono-Regular.ttf"; sourceTree = "<group>"; };
5DF0F70300000000F0F0F703 /* JetBrainsMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "JetBrainsMono-Bold.ttf"; sourceTree = "<group>"; };
5DF0F70500000000F0F0F705 /* MajorMonoDisplay-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "MajorMonoDisplay-Regular.ttf"; sourceTree = "<group>"; };
5DF0F70700000000F0F0F707 /* JetBrainsMono-OFL.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "JetBrainsMono-OFL.txt"; sourceTree = "<group>"; };
5DF0F70900000000F0F0F709 /* MajorMonoDisplay-OFL.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "MajorMonoDisplay-OFL.txt"; sourceTree = "<group>"; };
5DF51AB7285007EC007BCB83 /* DateFormatterUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterUtility.swift; sourceTree = "<group>"; };
5DF51AB92850310B007BCB83 /* ConferenceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConferenceRow.swift; sourceTree = "<group>"; };
5DF51ABB2851203F007BCB83 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -361,26 +361,6 @@
path = hackertracker;
sourceTree = "<group>";
};
5DF0F70B00000000F0F0F70B /* Fonts */ = {
isa = PBXGroup;
children = (
5DF0F70100000000F0F0F701 /* JetBrainsMono-Regular.ttf */,
5DF0F70300000000F0F0F703 /* JetBrainsMono-Bold.ttf */,
5DF0F70500000000F0F0F705 /* MajorMonoDisplay-Regular.ttf */,
5DF0F70700000000F0F0F707 /* JetBrainsMono-OFL.txt */,
5DF0F70900000000F0F0F709 /* MajorMonoDisplay-OFL.txt */,
);
path = Fonts;
sourceTree = "<group>";
};
5DF0F70C00000000F0F0F70C /* Resources */ = {
isa = PBXGroup;
children = (
5DF0F70B00000000F0F0F70B /* Fonts */,
);
path = Resources;
sourceTree = "<group>";
};
5DAE04B92820948E00930A2D /* Preview Content */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -512,6 +492,26 @@
path = Utils;
sourceTree = "<group>";
};
5DF0F70B00000000F0F0F70B /* Fonts */ = {
isa = PBXGroup;
children = (
5DF0F70100000000F0F0F701 /* JetBrainsMono-Regular.ttf */,
5DF0F70300000000F0F0F703 /* JetBrainsMono-Bold.ttf */,
5DF0F70500000000F0F0F705 /* MajorMonoDisplay-Regular.ttf */,
5DF0F70700000000F0F0F707 /* JetBrainsMono-OFL.txt */,
5DF0F70900000000F0F0F709 /* MajorMonoDisplay-OFL.txt */,
);
path = Fonts;
sourceTree = "<group>";
};
5DF0F70C00000000F0F0F70C /* Resources */ = {
isa = PBXGroup;
children = (
5DF0F70B00000000F0F0F70B /* Fonts */,
);
path = Resources;
sourceTree = "<group>";
};
5DF51AD928516869007BCB83 /* ViewModels */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -967,7 +967,7 @@
CODE_SIGN_ENTITLEMENTS = hackertracker/hackertracker.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 26062001;
CURRENT_PROJECT_VERSION = 26062701;
DEVELOPMENT_ASSET_PATHS = "\"hackertracker/Preview Content\"";
DEVELOPMENT_TEAM = MJ97FDMT75;
ENABLE_PREVIEWS = YES;
Expand Down Expand Up @@ -1007,7 +1007,7 @@
CODE_SIGN_ENTITLEMENTS = hackertracker/hackertracker.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 26062001;
CURRENT_PROJECT_VERSION = 26062701;
DEVELOPMENT_ASSET_PATHS = "\"hackertracker/Preview Content\"";
DEVELOPMENT_TEAM = MJ97FDMT75;
ENABLE_PREVIEWS = YES;
Expand Down
77 changes: 74 additions & 3 deletions hackertracker/Utils/Filters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,81 @@

import Foundation

/// Shared persistence helpers used by every filter store below.
private enum FilterStorePersistence {
static func loadIntSet(forKey key: String) -> Set<Int>? {
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
return try? JSONDecoder().decode(Set<Int>.self, from: data)
}

static func loadStringSet(forKey key: String) -> Set<String>? {
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
return try? JSONDecoder().decode(Set<String>.self, from: data)
}

static func save<T: Encodable>(_ value: T, forKey key: String) {
if let data = try? JSONEncoder().encode(value) {
UserDefaults.standard.set(data, forKey: key)
}
}
}

class Filters: ObservableObject {
@Published var filters: Set<Int>

private static let userDefaultsKey = "filtersStore.schedule.v1"
@Published var filters: Set<Int> {
didSet { FilterStorePersistence.save(filters, forKey: Self.userDefaultsKey) }
}

init(filters: Set<Int>) {
self.filters = filters
// Persisted value (if any) wins over the caller's default.
// Survives cold launch so users don't have to re-pick chips
// every time they relaunch the app.
self.filters = FilterStorePersistence.loadIntSet(forKey: Self.userDefaultsKey) ?? filters
}
}

/// Independent filter set for the Speakers list. Distinct class from
/// `Filters` so both can sit in the SwiftUI environment without type
/// collision — `Filters` continues to drive Schedule + All Content
/// while `SpeakerFiltersStore` is read only by SpeakersView and the
/// speaker filter sheet.
final class SpeakerFiltersStore: ObservableObject {
private static let userDefaultsKey = "filtersStore.speakers.v1"
@Published var filters: Set<Int> {
didSet { FilterStorePersistence.save(filters, forKey: Self.userDefaultsKey) }
}

init(filters: Set<Int> = []) {
self.filters = FilterStorePersistence.loadIntSet(forKey: Self.userDefaultsKey) ?? filters
}
}

/// Independent filter set for the Merch (Products) list. Holds the
/// selected size labels rather than tag ids; otherwise identical to
/// the other stores. Hoisted out of ProductsView's @State so the
/// selection survives tab switches in addition to cold launches.
final class MerchFiltersStore: ObservableObject {
private static let userDefaultsKey = "filtersStore.merch.v1"
@Published var sizes: Set<String> {
didSet { FilterStorePersistence.save(sizes, forKey: Self.userDefaultsKey) }
}

init(sizes: Set<String> = []) {
self.sizes = FilterStorePersistence.loadStringSet(forKey: Self.userDefaultsKey) ?? sizes
}
}

/// Configuration knobs shared between `SpeakerRow` (chip strip) and
/// `SpeakersView` (filter sheet + filter pipeline) so all three stay
/// consistent.
enum SpeakerListConfig {
/// Tagtype labels intentionally hidden from the speakers list.
/// These dimensions live on events (talks have a skill level and a
/// modality) but don't read as useful speaker metadata — a
/// speaker isn't "Beginner" or "Hybrid", their *talk* is. Drop
/// them from both the chip rollup and the filter sheet so users
/// see only the categorical/organizational signals.
/// "Tool" is included on the same logic: events tagged "Tool" are
/// tooling releases / demos, but the speaker isn't a tool.
static let excludedTagTypeLabels: Set<String> = ["Skill Level", "Modality", "Tool"]
}
6 changes: 5 additions & 1 deletion hackertracker/Utils/NotificationUtility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
//

import Foundation
import UserNotifications
// Swift 6 strict concurrency: UNNotificationRequest isn't yet annotated
// Sendable in UserNotifications. Treat the framework's Sendable
// diagnostics as warnings so the existing call sites (notably the
// addNotification closure capture below) keep compiling clean.
@preconcurrency import UserNotifications

enum NotificationUtility {
/// Phase 1 fix: previously a DispatchSemaphore-blocking accessor that could deadlock
Expand Down
21 changes: 21 additions & 0 deletions hackertracker/Utils/Searchable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,27 @@ extension [Speaker] {
guard !text.isEmpty else { return self }
return filter { _searchMatches($0.name, needle: text) || _searchMatches($0.description, needle: text) }
}

/// Speaker search with the speaker's event titles as additional
/// match surface. Passing an `eventsById` dict lets the search
/// match against talk titles too — useful when the user is
/// looking up "who's giving the BadgeLife panel" rather than the
/// speaker by name. Falls back gracefully when an event id isn't
/// in the dict (cold load), in which case that event just
/// doesn't contribute to the match for this speaker.
func search(text: String, eventsById: [Int: Event]) -> Self {
guard !text.isEmpty else { return self }
return filter { speaker in
if _searchMatches(speaker.name, needle: text)
|| _searchMatches(speaker.description, needle: text) {
return true
}
return speaker.eventIds.contains { id in
guard let title = eventsById[id]?.title else { return false }
return _searchMatches(title, needle: text)
}
}
}
}

extension [Content] {
Expand Down
Loading