From 4870b202d6cca14b34346e2260aa95c59aeea284 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Fri, 22 Sep 2023 19:33:53 +0200 Subject: [PATCH] Migrate TagGroup to SwiftData --- IceCubesApp/App/AppRegistry.swift | 16 ++++-- .../App/Tabs/Settings/SettingsTab.swift | 10 ++-- .../App/Tabs/Timeline/EditTagGroupView.swift | 31 +++++------ .../App/Tabs/Timeline/TimelineTab.swift | 27 +++++++--- .../Env/Sources/Env/UserPreferences.swift | 7 --- .../Sources/Models/SwiftData/TagGroup.swift | 28 ++++++++++ Packages/Models/Sources/Models/Tag.swift | 17 ------ .../Sources/Timeline/TimelineFilter.swift | 53 +++++++++++-------- .../Sources/Timeline/TimelineView.swift | 20 +++---- .../Sources/Timeline/TimelineViewModel.swift | 7 --- 10 files changed, 120 insertions(+), 96 deletions(-) create mode 100644 Packages/Models/Sources/Models/SwiftData/TagGroup.swift diff --git a/IceCubesApp/App/AppRegistry.swift b/IceCubesApp/App/AppRegistry.swift index 2afcf648..ec6ef2c0 100644 --- a/IceCubesApp/App/AppRegistry.swift +++ b/IceCubesApp/App/AppRegistry.swift @@ -31,9 +31,15 @@ extension View { case let .conversationDetail(conversation): ConversationDetailView(conversation: conversation) case let .hashTag(tag, accountId): - TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)), scrollToTopSignal: .constant(0), canFilterTimeline: false) + TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)), + selectedTagGroup: .constant(nil), + scrollToTopSignal: .constant(0), + canFilterTimeline: false) case let .list(list): - TimelineView(timeline: .constant(.list(list: list)), scrollToTopSignal: .constant(0), canFilterTimeline: false) + TimelineView(timeline: .constant(.list(list: list)), + selectedTagGroup: .constant(nil), + scrollToTopSignal: .constant(0), + canFilterTimeline: false) case let .following(id): AccountsListView(mode: .following(accountId: id)) case let .followers(id): @@ -45,7 +51,10 @@ extension View { case let .accountsList(accounts): AccountsListView(mode: .accountsList(accounts: accounts)) case .trendingTimeline: - TimelineView(timeline: .constant(.trending), scrollToTopSignal: .constant(0), canFilterTimeline: false) + TimelineView(timeline: .constant(.trending), + selectedTagGroup: .constant(nil), + scrollToTopSignal: .constant(0), + canFilterTimeline: false) case let .tagsList(tags): TagsListView(tags: tags) } @@ -125,6 +134,7 @@ extension View { modelContainer(for: [ Draft.self, LocalTimeline.self, + TagGroup.self ]) } } diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index a0b038e8..5710891e 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -31,6 +31,7 @@ struct SettingsTabs: View { @Binding var popToRootTab: Tab @Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline] + @Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup] var body: some View { NavigationStack(path: $routerPath.path) { @@ -274,20 +275,17 @@ struct SettingsTabs: View { private var tagGroupsView: some View { Form { - ForEach(preferences.tagGroups, id: \.self) { group in - Label(group.title, systemImage: group.sfSymbolName) + ForEach(tagGroups) { group in + Label(group.title, systemImage: group.symbolName) .onTapGesture { routerPath.presentedSheet = .editTagGroup(tagGroup: group, onSaved: nil) } } .onDelete { indexes in if let index = indexes.first { - _ = preferences.tagGroups.remove(at: index) + context.delete(tagGroups[index]) } } - .onMove { source, destination in - preferences.tagGroups.move(fromOffsets: source, toOffset: destination) - } .listRowBackground(theme.primaryBackgroundColor) Button { diff --git a/IceCubesApp/App/Tabs/Timeline/EditTagGroupView.swift b/IceCubesApp/App/Tabs/Timeline/EditTagGroupView.swift index a00bca96..21f7d2c9 100644 --- a/IceCubesApp/App/Tabs/Timeline/EditTagGroupView.swift +++ b/IceCubesApp/App/Tabs/Timeline/EditTagGroupView.swift @@ -10,8 +10,8 @@ import SwiftUI @MainActor struct EditTagGroupView: View { @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var context - @Environment(UserPreferences.self) private var preferences @Environment(Theme.self) private var theme @State private var title: String = "" @@ -22,7 +22,7 @@ struct EditTagGroupView: View { private var editingTagGroup: TagGroup? private var onSaved: ((TagGroup) -> Void)? - + private var canSave: Bool { !title.isEmpty && // At least have 2 tags, one main and one additional. @@ -70,7 +70,7 @@ struct EditTagGroupView: View { focusedField = .title if let editingTagGroup { title = editingTagGroup.title - sfSymbolName = editingTagGroup.sfSymbolName + sfSymbolName = editingTagGroup.symbolName tags = editingTagGroup.tags } } @@ -162,25 +162,20 @@ struct EditTagGroupView: View { } private func save() { - var toSave = tags - let main = toSave.removeFirst() - - let tagGroup: TagGroup = .init( - title: title.trimmingCharacters(in: .whitespaces), - sfSymbolName: sfSymbolName, - main: main, - additional: toSave - ) - if let editingTagGroup, - let index = preferences.tagGroups.firstIndex(of: editingTagGroup) - { - preferences.tagGroups[index] = tagGroup + if let editingTagGroup { + editingTagGroup.title = title + editingTagGroup.symbolName = sfSymbolName + editingTagGroup.tags = tags + onSaved?(editingTagGroup) } else { - preferences.tagGroups.append(tagGroup) + let tagGroup = TagGroup(title: title.trimmingCharacters(in: .whitespacesAndNewlines), + symbolName: sfSymbolName, + tags: tags) + context.insert(tagGroup) + onSaved?(tagGroup) } dismiss() - onSaved?(tagGroup) } @ViewBuilder diff --git a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift index 2ff34e4a..329f5136 100644 --- a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift @@ -22,12 +22,15 @@ struct TimelineTab: View { @State private var didAppear: Bool = false @State private var timeline: TimelineFilter = .home + @State private var selectedTagGroup: TagGroup? @State private var scrollToTopSignal: Int = 0 @Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline] - + @Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup] @AppStorage("remote_local_timeline") var legacyLocalTimelines: [String] = [] - @AppStorage("last_timeline_filter") var lastTimelineFilter: TimelineFilter = .home + @AppStorage("tag_groups") var legacyTagGroups: [LegacyTagGroup] = [] + + @AppStorage("last_timeline_filter") var lastTimelineFilter: TimelineFilter = .home private let canFilterTimeline: Bool @@ -39,7 +42,10 @@ struct TimelineTab: View { var body: some View { NavigationStack(path: $routerPath.path) { - TimelineView(timeline: $timeline, scrollToTopSignal: $scrollToTopSignal, canFilterTimeline: canFilterTimeline) + TimelineView(timeline: $timeline, + selectedTagGroup: $selectedTagGroup, + scrollToTopSignal: $scrollToTopSignal, + canFilterTimeline: canFilterTimeline) .withAppRouter() .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) .toolbar { @@ -50,6 +56,7 @@ struct TimelineTab: View { } .onAppear { migrateUserPreferencesTimeline() + migrateUserPreferencesTagGroups() routerPath.client = client if !didAppear, canFilterTimeline { didAppear = true @@ -153,12 +160,13 @@ struct TimelineTab: View { } Menu("timeline.filter.tag-groups") { - ForEach(preferences.tagGroups, id: \.self) { group in + ForEach(tagGroups) { group in Button { - timeline = .tagGroup(group) + selectedTagGroup = group + timeline = .tagGroup(title: group.title, tags: group.tags) } label: { VStack { - let icon = group.sfSymbolName.isEmpty ? "number" : group.sfSymbolName + let icon = group.symbolName.isEmpty ? "number" : group.symbolName Label(group.title, systemImage: icon) } } @@ -249,4 +257,11 @@ struct TimelineTab: View { } legacyLocalTimelines = [] } + + func migrateUserPreferencesTagGroups() { + for group in legacyTagGroups { + context.insert(TagGroup(title: group.title, symbolName: group.sfSymbolName, tags: group.tags)) + } + legacyTagGroups = [] + } } diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index 4f9e0282..8987cb3b 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -7,7 +7,6 @@ import SwiftUI @MainActor @Observable public class UserPreferences { class Storage { - @AppStorage("tag_groups") public var tagGroups: [TagGroup] = [] @AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari @AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true @AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true @@ -62,11 +61,6 @@ import SwiftUI private var client: Client? - public var tagGroups: [TagGroup] { - didSet { - storage.tagGroups = tagGroups - } - } public var preferredBrowser: PreferredBrowser { didSet { storage.preferredBrowser = preferredBrowser @@ -365,7 +359,6 @@ import SwiftUI } private init() { - tagGroups = storage.tagGroups preferredBrowser = storage.preferredBrowser showTranslateButton = storage.showTranslateButton isOpenAIEnabled = storage.isOpenAIEnabled diff --git a/Packages/Models/Sources/Models/SwiftData/TagGroup.swift b/Packages/Models/Sources/Models/SwiftData/TagGroup.swift new file mode 100644 index 00000000..17a589cc --- /dev/null +++ b/Packages/Models/Sources/Models/SwiftData/TagGroup.swift @@ -0,0 +1,28 @@ +import SwiftData +import SwiftUI +import Foundation + +@Model public class TagGroup: Equatable { + public var title: String + public var symbolName: String + public var tags: [String] + public var creationDate: Date + + public init(title: String, symbolName: String, tags: [String]) { + self.title = title + self.symbolName = symbolName + self.tags = tags + self.creationDate = Date() + } +} + +public struct LegacyTagGroup: Codable, Equatable, Hashable { + public let title: String + public let sfSymbolName: String + public let main: String + public let additional: [String] + + public var tags: [String] { + [main] + additional + } +} diff --git a/Packages/Models/Sources/Models/Tag.swift b/Packages/Models/Sources/Models/Tag.swift index 95582a8b..68991acd 100644 --- a/Packages/Models/Sources/Models/Tag.swift +++ b/Packages/Models/Sources/Models/Tag.swift @@ -63,20 +63,3 @@ extension Tag: Sendable {} extension Tag.History: Sendable {} extension FeaturedTag: Sendable {} -public struct TagGroup: Codable, Equatable, Hashable { - public init(title: String, sfSymbolName: String, main: String, additional: [String]) { - self.title = title - self.sfSymbolName = sfSymbolName - self.main = main - self.additional = additional - } - - public let title: String - public let sfSymbolName: String - public let main: String - public let additional: [String] - - public var tags: [String] { - [main] + additional - } -} diff --git a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift index a2763722..d8286000 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift @@ -32,7 +32,7 @@ public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable { public enum TimelineFilter: Hashable, Equatable { case home, local, federated, trending case hashtag(tag: String, accountId: String?) - case tagGroup(TagGroup) + case tagGroup(title: String, tags: [String]) case list(list: Models.List) case remoteLocal(server: String, filter: RemoteTimelineFilter) case latest @@ -73,8 +73,8 @@ public enum TimelineFilter: Hashable, Equatable { "Home" case let .hashtag(tag, _): "#\(tag)" - case let .tagGroup(group): - group.title + case let .tagGroup(title, _): + title case let .list(list): list.title case let .remoteLocal(server, _): @@ -96,8 +96,8 @@ public enum TimelineFilter: Hashable, Equatable { "timeline.home" case let .hashtag(tag, _): "#\(tag)" - case let .tagGroup(group): - LocalizedStringKey(group.title) // ?? not sure since this can't be localized. + case let .tagGroup(title, _): + LocalizedStringKey(title) // ?? not sure since this can't be localized. case let .list(list): LocalizedStringKey(list.title) case let .remoteLocal(server, _): @@ -128,29 +128,31 @@ public enum TimelineFilter: Hashable, Equatable { public func endpoint(sinceId: String?, maxId: String?, minId: String?, offset: Int?) -> Endpoint { switch self { - case .federated: Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false) - case .local: Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true) + case .federated: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false) + case .local: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true) case let .remoteLocal(_, filter): switch filter { case .local: - Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true) + return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true) case .federated: - Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false) + return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false) case .trending: - Trends.statuses(offset: offset) + return Trends.statuses(offset: offset) } - case .latest: Timelines.home(sinceId: nil, maxId: nil, minId: nil) - case .home: Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId) - case .trending: Trends.statuses(offset: offset) - case let .list(list): Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId) + case .latest: return Timelines.home(sinceId: nil, maxId: nil, minId: nil) + case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId) + case .trending: return Trends.statuses(offset: offset) + case let .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId) case let .hashtag(tag, accountId): if let accountId { - Accounts.statuses(id: accountId, sinceId: nil, tag: tag, onlyMedia: nil, excludeReplies: nil, pinned: nil) + return Accounts.statuses(id: accountId, sinceId: nil, tag: tag, onlyMedia: nil, excludeReplies: nil, pinned: nil) } else { - Timelines.hashtag(tag: tag, additional: nil, maxId: maxId) + return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId) } - case let .tagGroup(group): - Timelines.hashtag(tag: group.main, additional: group.additional, maxId: maxId) + case let .tagGroup(_, tags): + var tags = tags + tags.removeFirst() + return Timelines.hashtag(tag: tags.first ?? "", additional: tags, maxId: maxId) } } } @@ -189,8 +191,13 @@ extension TimelineFilter: Codable { accountId: accountId ) case .tagGroup: - let group = try container.decode(TagGroup.self, forKey: .tagGroup) - self = .tagGroup(group) + var nestedContainer = try container.nestedUnkeyedContainer(forKey: .hashtag) + let title = try nestedContainer.decode(String.self) + let tags = try nestedContainer.decode([String].self) + self = .tagGroup( + title: title, + tags: tags + ) case .list: let list = try container.decode( Models.List.self, @@ -232,8 +239,10 @@ extension TimelineFilter: Codable { var nestedContainer = container.nestedUnkeyedContainer(forKey: .hashtag) try nestedContainer.encode(tag) try nestedContainer.encode(accountId) - case let .tagGroup(group): - try container.encode(group, forKey: .tagGroup) + case let .tagGroup(title, tags): + var nestedContainer = container.nestedUnkeyedContainer(forKey: .tagGroup) + try nestedContainer.encode(title) + try nestedContainer.encode(tags) case let .list(list): try container.encode(list, forKey: .list) case let .remoteLocal(server, filter): diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index aa995314..1489fd58 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -27,11 +27,15 @@ public struct TimelineView: View { @State private var collectionView: UICollectionView? @Binding var timeline: TimelineFilter + @Binding var selectedTagGroup: TagGroup? @Binding var scrollToTopSignal: Int private let canFilterTimeline: Bool - public init(timeline: Binding, scrollToTopSignal: Binding, canFilterTimeline: Bool) { + public init(timeline: Binding, + selectedTagGroup: Binding, + scrollToTopSignal: Binding, canFilterTimeline: Bool) { _timeline = timeline + _selectedTagGroup = selectedTagGroup _scrollToTopSignal = scrollToTopSignal self.canFilterTimeline = canFilterTimeline } @@ -40,13 +44,9 @@ public struct TimelineView: View { ScrollViewReader { proxy in ZStack(alignment: .top) { List { - if viewModel.tagGroup != nil { - tagGroupHeaderView - } else if viewModel.tag == nil { - scrollToTopView - } else { - tagHeaderView - } + scrollToTopView + tagGroupHeaderView + tagHeaderView switch viewModel.timeline { case .remoteLocal: StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath, isRemote: true) @@ -211,7 +211,7 @@ public struct TimelineView: View { @ViewBuilder private var tagGroupHeaderView: some View { - if let group = viewModel.tagGroup { + if let group = selectedTagGroup { headerView { HStack { ScrollView(.horizontal) { @@ -230,7 +230,7 @@ public struct TimelineView: View { .scrollIndicators(.hidden) Button("status.action.edit") { routerPath.presentedSheet = .editTagGroup(tagGroup: group, onSaved: { group in - viewModel.timeline = .tagGroup(group) + viewModel.timeline = .tagGroup(title: group.title, tags: group.tags) }) } .buttonStyle(.bordered) diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index 11486a4f..8c0959fa 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -42,13 +42,6 @@ import SwiftUI var tag: Tag? - var tagGroup: TagGroup? { - if case let .tagGroup(group) = timeline { - return group - } - return nil - } - // Internal source of truth for a timeline. private var datasource = TimelineDatasource() private let cache = TimelineCache()