Migrate TagGroup to SwiftData

This commit is contained in:
Thomas Ricouard 2023-09-22 19:33:53 +02:00
parent 527d982dce
commit 4870b202d6
10 changed files with 120 additions and 96 deletions

View file

@ -31,9 +31,15 @@ extension View {
case let .conversationDetail(conversation): case let .conversationDetail(conversation):
ConversationDetailView(conversation: conversation) ConversationDetailView(conversation: conversation)
case let .hashTag(tag, accountId): 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): 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): case let .following(id):
AccountsListView(mode: .following(accountId: id)) AccountsListView(mode: .following(accountId: id))
case let .followers(id): case let .followers(id):
@ -45,7 +51,10 @@ extension View {
case let .accountsList(accounts): case let .accountsList(accounts):
AccountsListView(mode: .accountsList(accounts: accounts)) AccountsListView(mode: .accountsList(accounts: accounts))
case .trendingTimeline: 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): case let .tagsList(tags):
TagsListView(tags: tags) TagsListView(tags: tags)
} }
@ -125,6 +134,7 @@ extension View {
modelContainer(for: [ modelContainer(for: [
Draft.self, Draft.self,
LocalTimeline.self, LocalTimeline.self,
TagGroup.self
]) ])
} }
} }

View file

@ -31,6 +31,7 @@ struct SettingsTabs: View {
@Binding var popToRootTab: Tab @Binding var popToRootTab: Tab
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline] @Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
var body: some View { var body: some View {
NavigationStack(path: $routerPath.path) { NavigationStack(path: $routerPath.path) {
@ -274,20 +275,17 @@ struct SettingsTabs: View {
private var tagGroupsView: some View { private var tagGroupsView: some View {
Form { Form {
ForEach(preferences.tagGroups, id: \.self) { group in ForEach(tagGroups) { group in
Label(group.title, systemImage: group.sfSymbolName) Label(group.title, systemImage: group.symbolName)
.onTapGesture { .onTapGesture {
routerPath.presentedSheet = .editTagGroup(tagGroup: group, onSaved: nil) routerPath.presentedSheet = .editTagGroup(tagGroup: group, onSaved: nil)
} }
} }
.onDelete { indexes in .onDelete { indexes in
if let index = indexes.first { 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) .listRowBackground(theme.primaryBackgroundColor)
Button { Button {

View file

@ -10,8 +10,8 @@ import SwiftUI
@MainActor @MainActor
struct EditTagGroupView: View { struct EditTagGroupView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var context
@Environment(UserPreferences.self) private var preferences
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@State private var title: String = "" @State private var title: String = ""
@ -22,7 +22,7 @@ struct EditTagGroupView: View {
private var editingTagGroup: TagGroup? private var editingTagGroup: TagGroup?
private var onSaved: ((TagGroup) -> Void)? private var onSaved: ((TagGroup) -> Void)?
private var canSave: Bool { private var canSave: Bool {
!title.isEmpty && !title.isEmpty &&
// At least have 2 tags, one main and one additional. // At least have 2 tags, one main and one additional.
@ -70,7 +70,7 @@ struct EditTagGroupView: View {
focusedField = .title focusedField = .title
if let editingTagGroup { if let editingTagGroup {
title = editingTagGroup.title title = editingTagGroup.title
sfSymbolName = editingTagGroup.sfSymbolName sfSymbolName = editingTagGroup.symbolName
tags = editingTagGroup.tags tags = editingTagGroup.tags
} }
} }
@ -162,25 +162,20 @@ struct EditTagGroupView: View {
} }
private func save() { private func save() {
var toSave = tags if let editingTagGroup {
let main = toSave.removeFirst() editingTagGroup.title = title
editingTagGroup.symbolName = sfSymbolName
let tagGroup: TagGroup = .init( editingTagGroup.tags = tags
title: title.trimmingCharacters(in: .whitespaces), onSaved?(editingTagGroup)
sfSymbolName: sfSymbolName,
main: main,
additional: toSave
)
if let editingTagGroup,
let index = preferences.tagGroups.firstIndex(of: editingTagGroup)
{
preferences.tagGroups[index] = tagGroup
} else { } else {
preferences.tagGroups.append(tagGroup) let tagGroup = TagGroup(title: title.trimmingCharacters(in: .whitespacesAndNewlines),
symbolName: sfSymbolName,
tags: tags)
context.insert(tagGroup)
onSaved?(tagGroup)
} }
dismiss() dismiss()
onSaved?(tagGroup)
} }
@ViewBuilder @ViewBuilder

View file

@ -22,12 +22,15 @@ struct TimelineTab: View {
@State private var didAppear: Bool = false @State private var didAppear: Bool = false
@State private var timeline: TimelineFilter = .home @State private var timeline: TimelineFilter = .home
@State private var selectedTagGroup: TagGroup?
@State private var scrollToTopSignal: Int = 0 @State private var scrollToTopSignal: Int = 0
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline] @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("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 private let canFilterTimeline: Bool
@ -39,7 +42,10 @@ struct TimelineTab: View {
var body: some View { var body: some View {
NavigationStack(path: $routerPath.path) { NavigationStack(path: $routerPath.path) {
TimelineView(timeline: $timeline, scrollToTopSignal: $scrollToTopSignal, canFilterTimeline: canFilterTimeline) TimelineView(timeline: $timeline,
selectedTagGroup: $selectedTagGroup,
scrollToTopSignal: $scrollToTopSignal,
canFilterTimeline: canFilterTimeline)
.withAppRouter() .withAppRouter()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
.toolbar { .toolbar {
@ -50,6 +56,7 @@ struct TimelineTab: View {
} }
.onAppear { .onAppear {
migrateUserPreferencesTimeline() migrateUserPreferencesTimeline()
migrateUserPreferencesTagGroups()
routerPath.client = client routerPath.client = client
if !didAppear, canFilterTimeline { if !didAppear, canFilterTimeline {
didAppear = true didAppear = true
@ -153,12 +160,13 @@ struct TimelineTab: View {
} }
Menu("timeline.filter.tag-groups") { Menu("timeline.filter.tag-groups") {
ForEach(preferences.tagGroups, id: \.self) { group in ForEach(tagGroups) { group in
Button { Button {
timeline = .tagGroup(group) selectedTagGroup = group
timeline = .tagGroup(title: group.title, tags: group.tags)
} label: { } label: {
VStack { VStack {
let icon = group.sfSymbolName.isEmpty ? "number" : group.sfSymbolName let icon = group.symbolName.isEmpty ? "number" : group.symbolName
Label(group.title, systemImage: icon) Label(group.title, systemImage: icon)
} }
} }
@ -249,4 +257,11 @@ struct TimelineTab: View {
} }
legacyLocalTimelines = [] legacyLocalTimelines = []
} }
func migrateUserPreferencesTagGroups() {
for group in legacyTagGroups {
context.insert(TagGroup(title: group.title, symbolName: group.sfSymbolName, tags: group.tags))
}
legacyTagGroups = []
}
} }

View file

@ -7,7 +7,6 @@ import SwiftUI
@MainActor @MainActor
@Observable public class UserPreferences { @Observable public class UserPreferences {
class Storage { class Storage {
@AppStorage("tag_groups") public var tagGroups: [TagGroup] = []
@AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari @AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari
@AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true @AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true
@AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true @AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true
@ -62,11 +61,6 @@ import SwiftUI
private var client: Client? private var client: Client?
public var tagGroups: [TagGroup] {
didSet {
storage.tagGroups = tagGroups
}
}
public var preferredBrowser: PreferredBrowser { public var preferredBrowser: PreferredBrowser {
didSet { didSet {
storage.preferredBrowser = preferredBrowser storage.preferredBrowser = preferredBrowser
@ -365,7 +359,6 @@ import SwiftUI
} }
private init() { private init() {
tagGroups = storage.tagGroups
preferredBrowser = storage.preferredBrowser preferredBrowser = storage.preferredBrowser
showTranslateButton = storage.showTranslateButton showTranslateButton = storage.showTranslateButton
isOpenAIEnabled = storage.isOpenAIEnabled isOpenAIEnabled = storage.isOpenAIEnabled

View file

@ -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
}
}

View file

@ -63,20 +63,3 @@ extension Tag: Sendable {}
extension Tag.History: Sendable {} extension Tag.History: Sendable {}
extension FeaturedTag: 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
}
}

View file

@ -32,7 +32,7 @@ public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable {
public enum TimelineFilter: Hashable, Equatable { public enum TimelineFilter: Hashable, Equatable {
case home, local, federated, trending case home, local, federated, trending
case hashtag(tag: String, accountId: String?) case hashtag(tag: String, accountId: String?)
case tagGroup(TagGroup) case tagGroup(title: String, tags: [String])
case list(list: Models.List) case list(list: Models.List)
case remoteLocal(server: String, filter: RemoteTimelineFilter) case remoteLocal(server: String, filter: RemoteTimelineFilter)
case latest case latest
@ -73,8 +73,8 @@ public enum TimelineFilter: Hashable, Equatable {
"Home" "Home"
case let .hashtag(tag, _): case let .hashtag(tag, _):
"#\(tag)" "#\(tag)"
case let .tagGroup(group): case let .tagGroup(title, _):
group.title title
case let .list(list): case let .list(list):
list.title list.title
case let .remoteLocal(server, _): case let .remoteLocal(server, _):
@ -96,8 +96,8 @@ public enum TimelineFilter: Hashable, Equatable {
"timeline.home" "timeline.home"
case let .hashtag(tag, _): case let .hashtag(tag, _):
"#\(tag)" "#\(tag)"
case let .tagGroup(group): case let .tagGroup(title, _):
LocalizedStringKey(group.title) // ?? not sure since this can't be localized. LocalizedStringKey(title) // ?? not sure since this can't be localized.
case let .list(list): case let .list(list):
LocalizedStringKey(list.title) LocalizedStringKey(list.title)
case let .remoteLocal(server, _): 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 { public func endpoint(sinceId: String?, maxId: String?, minId: String?, offset: Int?) -> Endpoint {
switch self { switch self {
case .federated: Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false) case .federated: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false)
case .local: Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true) case .local: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true)
case let .remoteLocal(_, filter): case let .remoteLocal(_, filter):
switch filter { switch filter {
case .local: 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: 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: case .trending:
Trends.statuses(offset: offset) return Trends.statuses(offset: offset)
} }
case .latest: Timelines.home(sinceId: nil, maxId: nil, minId: nil) case .latest: return Timelines.home(sinceId: nil, maxId: nil, minId: nil)
case .home: Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId) case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId)
case .trending: Trends.statuses(offset: offset) case .trending: return Trends.statuses(offset: offset)
case let .list(list): Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId) case let .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId)
case let .hashtag(tag, accountId): case let .hashtag(tag, accountId):
if let 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 { } else {
Timelines.hashtag(tag: tag, additional: nil, maxId: maxId) return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId)
} }
case let .tagGroup(group): case let .tagGroup(_, tags):
Timelines.hashtag(tag: group.main, additional: group.additional, maxId: maxId) var tags = tags
tags.removeFirst()
return Timelines.hashtag(tag: tags.first ?? "", additional: tags, maxId: maxId)
} }
} }
} }
@ -189,8 +191,13 @@ extension TimelineFilter: Codable {
accountId: accountId accountId: accountId
) )
case .tagGroup: case .tagGroup:
let group = try container.decode(TagGroup.self, forKey: .tagGroup) var nestedContainer = try container.nestedUnkeyedContainer(forKey: .hashtag)
self = .tagGroup(group) let title = try nestedContainer.decode(String.self)
let tags = try nestedContainer.decode([String].self)
self = .tagGroup(
title: title,
tags: tags
)
case .list: case .list:
let list = try container.decode( let list = try container.decode(
Models.List.self, Models.List.self,
@ -232,8 +239,10 @@ extension TimelineFilter: Codable {
var nestedContainer = container.nestedUnkeyedContainer(forKey: .hashtag) var nestedContainer = container.nestedUnkeyedContainer(forKey: .hashtag)
try nestedContainer.encode(tag) try nestedContainer.encode(tag)
try nestedContainer.encode(accountId) try nestedContainer.encode(accountId)
case let .tagGroup(group): case let .tagGroup(title, tags):
try container.encode(group, forKey: .tagGroup) var nestedContainer = container.nestedUnkeyedContainer(forKey: .tagGroup)
try nestedContainer.encode(title)
try nestedContainer.encode(tags)
case let .list(list): case let .list(list):
try container.encode(list, forKey: .list) try container.encode(list, forKey: .list)
case let .remoteLocal(server, filter): case let .remoteLocal(server, filter):

View file

@ -27,11 +27,15 @@ public struct TimelineView: View {
@State private var collectionView: UICollectionView? @State private var collectionView: UICollectionView?
@Binding var timeline: TimelineFilter @Binding var timeline: TimelineFilter
@Binding var selectedTagGroup: TagGroup?
@Binding var scrollToTopSignal: Int @Binding var scrollToTopSignal: Int
private let canFilterTimeline: Bool private let canFilterTimeline: Bool
public init(timeline: Binding<TimelineFilter>, scrollToTopSignal: Binding<Int>, canFilterTimeline: Bool) { public init(timeline: Binding<TimelineFilter>,
selectedTagGroup: Binding<TagGroup?>,
scrollToTopSignal: Binding<Int>, canFilterTimeline: Bool) {
_timeline = timeline _timeline = timeline
_selectedTagGroup = selectedTagGroup
_scrollToTopSignal = scrollToTopSignal _scrollToTopSignal = scrollToTopSignal
self.canFilterTimeline = canFilterTimeline self.canFilterTimeline = canFilterTimeline
} }
@ -40,13 +44,9 @@ public struct TimelineView: View {
ScrollViewReader { proxy in ScrollViewReader { proxy in
ZStack(alignment: .top) { ZStack(alignment: .top) {
List { List {
if viewModel.tagGroup != nil { scrollToTopView
tagGroupHeaderView tagGroupHeaderView
} else if viewModel.tag == nil { tagHeaderView
scrollToTopView
} else {
tagHeaderView
}
switch viewModel.timeline { switch viewModel.timeline {
case .remoteLocal: case .remoteLocal:
StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath, isRemote: true) StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath, isRemote: true)
@ -211,7 +211,7 @@ public struct TimelineView: View {
@ViewBuilder @ViewBuilder
private var tagGroupHeaderView: some View { private var tagGroupHeaderView: some View {
if let group = viewModel.tagGroup { if let group = selectedTagGroup {
headerView { headerView {
HStack { HStack {
ScrollView(.horizontal) { ScrollView(.horizontal) {
@ -230,7 +230,7 @@ public struct TimelineView: View {
.scrollIndicators(.hidden) .scrollIndicators(.hidden)
Button("status.action.edit") { Button("status.action.edit") {
routerPath.presentedSheet = .editTagGroup(tagGroup: group, onSaved: { group in routerPath.presentedSheet = .editTagGroup(tagGroup: group, onSaved: { group in
viewModel.timeline = .tagGroup(group) viewModel.timeline = .tagGroup(title: group.title, tags: group.tags)
}) })
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)

View file

@ -42,13 +42,6 @@ import SwiftUI
var tag: Tag? var tag: Tag?
var tagGroup: TagGroup? {
if case let .tagGroup(group) = timeline {
return group
}
return nil
}
// Internal source of truth for a timeline. // Internal source of truth for a timeline.
private var datasource = TimelineDatasource() private var datasource = TimelineDatasource()
private let cache = TimelineCache() private let cache = TimelineCache()