diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index f8736902..e5d96656 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -47,7 +47,7 @@ 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4629506F6600B3281A /* NotificationTab.swift */; }; 9F35DB4A29506FA100B3281A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB4929506FA100B3281A /* Notifications */; }; 9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4B2952005C00B3281A /* MessagesTab.swift */; }; - 9F398AA62935FE8A00A889F2 /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AA52935FE8A00A889F2 /* AppRouter.swift */; }; + 9F398AA62935FE8A00A889F2 /* AppRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AA52935FE8A00A889F2 /* AppRegistry.swift */; }; 9F398AA92935FFDB00A889F2 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AA82935FFDB00A889F2 /* Account */; }; 9F398AAB2935FFDB00A889F2 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AAA2935FFDB00A889F2 /* Models */; }; 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AB229360A4C00A889F2 /* TimelineTab.swift */; }; @@ -207,7 +207,7 @@ 9F35DB4B2952005C00B3281A /* MessagesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesTab.swift; sourceTree = ""; }; 9F38C233297D03120018F11E /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 9F398AA32935F90100A889F2 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = ""; }; - 9F398AA52935FE8A00A889F2 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = ""; }; + 9F398AA52935FE8A00A889F2 /* AppRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRegistry.swift; sourceTree = ""; }; 9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = ""; }; 9F398AB229360A4C00A889F2 /* TimelineTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTab.swift; sourceTree = ""; }; 9F4A48182976B21900A1A038 /* ProfileTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTab.swift; sourceTree = ""; }; @@ -377,7 +377,7 @@ 9F654BF0299AC46200D27FA5 /* Report */, 9FAE4AC9293783A200772766 /* Tabs */, 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */, - 9F398AA52935FE8A00A889F2 /* AppRouter.swift */, + 9F398AA52935FE8A00A889F2 /* AppRegistry.swift */, 639CDF9B296AC82F00C35E58 /* SafariRouter.swift */, 9FAD85A7297582F100496AB1 /* QuickLookRepresentable.swift */, 9FAD85CE2975B68900496AB1 /* SideBarView.swift */, @@ -842,7 +842,7 @@ FA31A9AB2A66BF7C00D5F662 /* EditTagGroupView.swift in Sources */, FAD203D02A66D8A80030A7FD /* Symbols.swift in Sources */, 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */, - 9F398AA62935FE8A00A889F2 /* AppRouter.swift in Sources */, + 9F398AA62935FE8A00A889F2 /* AppRegistry.swift in Sources */, 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */, 9F4A48192976B21900A1A038 /* ProfileTab.swift in Sources */, 9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */, diff --git a/IceCubesApp/App/AppRouter.swift b/IceCubesApp/App/AppRegistry.swift similarity index 98% rename from IceCubesApp/App/AppRouter.swift rename to IceCubesApp/App/AppRegistry.swift index 1ab36a82..8be25cf6 100644 --- a/IceCubesApp/App/AppRouter.swift +++ b/IceCubesApp/App/AppRegistry.swift @@ -120,6 +120,12 @@ extension View { .environment(PushNotificationsService.shared) .environment(AppAccountsManager.shared.currentClient) } + + func withModelContainer() -> some View { + modelContainer(for: [ + Draft.self, + ]) + } } struct ActivityView: UIViewControllerRepresentable { diff --git a/IceCubesApp/App/IceCubesApp.swift b/IceCubesApp/App/IceCubesApp.swift index b1e063a1..83aa9d23 100644 --- a/IceCubesApp/App/IceCubesApp.swift +++ b/IceCubesApp/App/IceCubesApp.swift @@ -8,6 +8,7 @@ import Network import RevenueCat import SwiftUI import Timeline +import Status @main struct IceCubesApp: App { @@ -75,6 +76,7 @@ struct IceCubesApp: App { } } } + .withModelContainer() } .commands { appMenu diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index ad4c18a6..2370dfc3 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -10,7 +10,6 @@ import SwiftUI @AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = [] @AppStorage("tag_groups") public var tagGroups: [TagGroup] = [] @AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari - @AppStorage("draft_posts") public var draftsPosts: [String] = [] @AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true @AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true @@ -79,11 +78,7 @@ import SwiftUI storage.preferredBrowser = preferredBrowser } } - public var draftsPosts: [String] { - didSet { - storage.draftsPosts = draftsPosts - } - } + public var showTranslateButton: Bool { didSet { storage.showTranslateButton = showTranslateButton @@ -379,7 +374,6 @@ import SwiftUI remoteLocalTimelines = storage.remoteLocalTimelines tagGroups = storage.tagGroups preferredBrowser = storage.preferredBrowser - draftsPosts = storage.draftsPosts showTranslateButton = storage.showTranslateButton isOpenAIEnabled = storage.isOpenAIEnabled recentlyUsedLanguages = storage.recentlyUsedLanguages diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift index 473e7187..f32f2df1 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift @@ -111,9 +111,10 @@ struct StatusEditorAccessoryView: View { .accessibilityLabel("accessibility.editor.button.drafts") .popover(isPresented: $isDraftsSheetDisplayed) { if UIDevice.current.userInterfaceIdiom == .phone { - draftsSheetView + draftsListView + .presentationDetents([.medium]) } else { - draftsSheetView + draftsListView .frame(width: 400, height: 500) } } @@ -176,6 +177,16 @@ struct StatusEditorAccessoryView: View { viewModel.setInitialLanguageSelection(preference: preferences.recentlyUsedLanguages.first ?? preferences.serverPreferences?.postLanguage) } } + + private var draftsListView: some View { + DraftsListView(selectedDraft: .init(get: { + nil + }, set: { draft in + if let draft { + viewModel.insertStatusText(text: draft.content) + } + })) + } @ViewBuilder private func languageTextView(isoCode: String, nativeName: String?, name: String?) -> some View { @@ -269,38 +280,6 @@ struct StatusEditorAccessoryView: View { } } - private var draftsSheetView: some View { - NavigationStack { - List { - ForEach(preferences.draftsPosts, id: \.self) { draft in - Button { - viewModel.insertStatusText(text: draft) - isDraftsSheetDisplayed = false - } label: { - Text(draft) - .lineLimit(3) - .foregroundStyle(theme.labelColor) - }.listRowBackground(theme.primaryBackgroundColor) - } - .onDelete { indexes in - if let index = indexes.first { - preferences.draftsPosts.remove(at: index) - } - } - } - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("action.cancel", action: { isDraftsSheetDisplayed = false }) - } - } - .scrollContentBackground(.hidden) - .background(theme.secondaryBackgroundColor) - .navigationTitle("status.editor.drafts.navigation-title") - .navigationBarTitleDisplayMode(.inline) - } - .presentationDetents([.medium]) - } - private var customEmojisSheet: some View { NavigationStack { ScrollView { diff --git a/Packages/Status/Sources/Status/Editor/Drafts/Draft.swift b/Packages/Status/Sources/Status/Editor/Drafts/Draft.swift new file mode 100644 index 00000000..1cd3ec83 --- /dev/null +++ b/Packages/Status/Sources/Status/Editor/Drafts/Draft.swift @@ -0,0 +1,15 @@ +import SwiftData +import SwiftUI +import Foundation + +@Model public class Draft: Identifiable { + @Attribute(.unique) public var id: UUID + public var content: String + public var creationDate: Date + + public init(content: String) { + self.id = UUID() + self.content = content + self.creationDate = Date() + } +} diff --git a/Packages/Status/Sources/Status/Editor/Drafts/DraftsListView.swift b/Packages/Status/Sources/Status/Editor/Drafts/DraftsListView.swift new file mode 100644 index 00000000..6c2c6759 --- /dev/null +++ b/Packages/Status/Sources/Status/Editor/Drafts/DraftsListView.swift @@ -0,0 +1,64 @@ +import SwiftUI +import SwiftData +import DesignSystem + +struct DraftsListView: View { + @AppStorage("draft_posts") public var legacyDraftPosts: [String] = [] + + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var context + + @Environment(Theme.self) private var theme + + @Query(sort: \Draft.creationDate, order: .reverse) var drafts: [Draft] + + @Binding var selectedDraft: Draft? + + var body: some View { + NavigationStack { + List { + ForEach(drafts) { draft in + Button { + selectedDraft = draft + dismiss() + } label: { + VStack(alignment: .leading, spacing: 8) { + Text(draft.content) + .font(.body) + .lineLimit(3) + .foregroundStyle(theme.labelColor) + Text(draft.creationDate, style: .relative) + .font(.footnote) + .foregroundStyle(.gray) + } + }.listRowBackground(theme.primaryBackgroundColor) + } + .onDelete { indexes in + if let index = indexes.first { + context.delete(drafts[index]) + } + } + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("action.cancel", action: { dismiss() }) + } + } + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) + .navigationTitle("status.editor.drafts.navigation-title") + .navigationBarTitleDisplayMode(.inline) + .onAppear { + migrateUserPreferencesDraft() + } + } + } + + func migrateUserPreferencesDraft() { + for draft in legacyDraftPosts { + let newDraft = Draft(content: draft) + context.insert(newDraft) + } + legacyDraftPosts = [] + } +} diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index 6abb055f..0e809e99 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -19,6 +19,7 @@ public struct StatusEditorView: View { @Environment(Client.self) private var client @Environment(CurrentAccount.self) private var currentAccount @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var context @State private var viewModel: StatusEditorViewModel @FocusState private var isSpoilerTextFocused: Bool @@ -144,22 +145,23 @@ public struct StatusEditorView: View { Text("action.cancel") } .keyboardShortcut(.cancelAction) - .confirmationDialog("", - isPresented: $isDismissAlertPresented, - actions: { - Button("status.draft.delete", role: .destructive) { - dismiss() - NotificationCenter.default.post(name: NotificationsName.shareSheetClose, - object: nil) - } - Button("status.draft.save") { - preferences.draftsPosts.insert(viewModel.statusText.string, at: 0) - dismiss() - NotificationCenter.default.post(name: NotificationsName.shareSheetClose, - object: nil) - } - Button("action.cancel", role: .cancel) {} - }) + .confirmationDialog( + "", + isPresented: $isDismissAlertPresented, + actions: { + Button("status.draft.delete", role: .destructive) { + dismiss() + NotificationCenter.default.post(name: NotificationsName.shareSheetClose, + object: nil) + } + Button("status.draft.save") { + context.insert(Draft(content: viewModel.statusText.string)) + dismiss() + NotificationCenter.default.post(name: NotificationsName.shareSheetClose, + object: nil) + } + Button("action.cancel", role: .cancel) {} + }) } } }