diff --git a/IceCubesApp/App/AppRegistry.swift b/IceCubesApp/App/AppRegistry.swift index 8de29b3f..43a3fde5 100644 --- a/IceCubesApp/App/AppRegistry.swift +++ b/IceCubesApp/App/AppRegistry.swift @@ -8,10 +8,10 @@ import LinkPresentation import Lists import MediaUI import Models +import Notifications import StatusKit import SwiftUI import Timeline -import Notifications @MainActor extension View { @@ -67,7 +67,7 @@ extension View { case .notificationsRequests: NotificationsRequestsListView() case let .notificationForAccount(accountId): - NotificationsListView(lockedType: nil , + NotificationsListView(lockedType: nil, lockedAccountId: accountId, scrollToTopSignal: .constant(0)) case .blockedAccounts: diff --git a/IceCubesApp/App/Main/IceCubesApp+Scene.swift b/IceCubesApp/App/Main/IceCubesApp+Scene.swift index 47857f41..c8832862 100644 --- a/IceCubesApp/App/Main/IceCubesApp+Scene.swift +++ b/IceCubesApp/App/Main/IceCubesApp+Scene.swift @@ -1,8 +1,8 @@ +import AppIntents import Env import MediaUI import StatusKit import SwiftUI -import AppIntents extension IceCubesApp { var appScene: some Scene { @@ -125,20 +125,21 @@ extension IceCubesApp { .defaultSize(width: 1200, height: 1000) .windowResizability(.contentMinSize) } - - private func handleIntent(_ intent: any AppIntent) { + + private func handleIntent(_: any AppIntent) { if let postIntent = appIntentService.handledIntent?.intent as? PostIntent { #if os(visionOS) || os(macOS) - openWindow(value: WindowDestinationEditor.prefilledStatusEditor(text: postIntent.content ?? "", - visibility: userPreferences.postVisibility)) + openWindow(value: WindowDestinationEditor.prefilledStatusEditor(text: postIntent.content ?? "", + visibility: userPreferences.postVisibility)) #else - appRouterPath.presentedSheet = .prefilledStatusEditor(text: postIntent.content ?? "", - visibility: userPreferences.postVisibility) + appRouterPath.presentedSheet = .prefilledStatusEditor(text: postIntent.content ?? "", + visibility: userPreferences.postVisibility) #endif } else if let tabIntent = appIntentService.handledIntent?.intent as? TabIntent { selectedTab = tabIntent.tab.toAppTab } else if let imageIntent = appIntentService.handledIntent?.intent as? PostImageIntent, - let urls = imageIntent.images?.compactMap({ $0.fileURL }) { + let urls = imageIntent.images?.compactMap({ $0.fileURL }) + { appRouterPath.presentedSheet = .imageURL(urls: urls, visibility: userPreferences.postVisibility) } diff --git a/IceCubesApp/App/Tabs/Tabs.swift b/IceCubesApp/App/Tabs/Tabs.swift index a7d450ac..d361115a 100644 --- a/IceCubesApp/App/Tabs/Tabs.swift +++ b/IceCubesApp/App/Tabs/Tabs.swift @@ -1,10 +1,10 @@ import Account +import AppIntents import DesignSystem import Explore import Foundation import StatusKit import SwiftUI -import AppIntents @MainActor enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable { diff --git a/IceCubesAppIntents/AppIntentService.swift b/IceCubesAppIntents/AppIntentService.swift index 6de561aa..81d365a0 100644 --- a/IceCubesAppIntents/AppIntentService.swift +++ b/IceCubesAppIntents/AppIntentService.swift @@ -1,5 +1,5 @@ -import SwiftUI import AppIntents +import SwiftUI @Observable public class AppIntentService: @unchecked Sendable { @@ -7,19 +7,19 @@ public class AppIntentService: @unchecked Sendable { static func == (lhs: AppIntentService.HandledIntent, rhs: AppIntentService.HandledIntent) -> Bool { lhs.id == rhs.id } - + let id: String let intent: any AppIntent - + init(intent: any AppIntent) { - self.id = UUID().uuidString + id = UUID().uuidString self.intent = intent } } - + public static let shared = AppIntentService() - + var handledIntent: HandledIntent? - - private init() { } + + private init() {} } diff --git a/IceCubesAppIntents/InlinePostIntent.swift b/IceCubesAppIntents/InlinePostIntent.swift index 8d854cc7..69c316ba 100644 --- a/IceCubesAppIntents/InlinePostIntent.swift +++ b/IceCubesAppIntents/InlinePostIntent.swift @@ -1,67 +1,63 @@ -import Foundation -import AppIntents import AppAccount -import Network +import AppIntents import Env +import Foundation import Models +import Network enum PostVisibility: String, AppEnum { case direct, priv, unlisted, pub - - public static var caseDisplayRepresentations: [PostVisibility : DisplayRepresentation] { + + public static var caseDisplayRepresentations: [PostVisibility: DisplayRepresentation] { [.direct: "Private", - .priv: "Followers Only", - .unlisted: "Quiet Public", - .pub: "Public"] + .priv: "Followers Only", + .unlisted: "Quiet Public", + .pub: "Public"] } - - static var typeDisplayName: LocalizedStringResource { - get { "Visibility" } - } - + + static var typeDisplayName: LocalizedStringResource { "Visibility" } + public static let typeDisplayRepresentation: TypeDisplayRepresentation = "Visibility" - + var toAppVisibility: Models.Visibility { switch self { case .direct: - .direct + .direct case .priv: - .priv + .priv case .unlisted: - .unlisted + .unlisted case .pub: - .pub + .pub } } } struct AppAccountWrapper: Identifiable, AppEntity { var id: String { account.id } - + let account: AppAccount - + static var defaultQuery = DefaultAppAccountQuery() - + static var typeDisplayRepresentation: TypeDisplayRepresentation = "AppAccount" - + var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(account.accountName ?? account.server)") } - } struct DefaultAppAccountQuery: EntityQuery { - func entities(for identifiers: [AppAccountWrapper.ID]) async throws -> [AppAccountWrapper] { return await AppAccountsManager.shared.availableAccounts.filter { account in identifiers.contains { id in id == account.id } - }.map{ AppAccountWrapper(account: $0 )} + }.map { AppAccountWrapper(account: $0) } } func suggestedEntities() async throws -> [AppAccountWrapper] { - await AppAccountsManager.shared.availableAccounts.map{ .init(account: $0)} + await AppAccountsManager.shared.availableAccounts.map { .init(account: $0) } } func defaultResult() async -> AppAccountWrapper? { @@ -72,21 +68,20 @@ struct DefaultAppAccountQuery: EntityQuery { struct InlinePostIntent: AppIntent { static let title: LocalizedStringResource = "Send text status to Mastodon" static var description: IntentDescription { - get { - "Send a text status to Mastodon using Ice Cubes" - } + "Send a text status to Mastodon using Ice Cubes" } + static let openAppWhenRun: Bool = false - + @Parameter(title: "Account", requestValueDialog: IntentDialog("Account")) var account: AppAccountWrapper - + @Parameter(title: "Post visibility", requestValueDialog: IntentDialog("Visibility of your post")) var visibility: PostVisibility - + @Parameter(title: "Post content", requestValueDialog: IntentDialog("Content of the post to be sent to Mastodon")) var content: String - + @MainActor func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView { let client = Client(server: account.account.server, version: .v1, oauthToken: account.account.oauthToken) diff --git a/IceCubesAppIntents/PostImageIntent.swift b/IceCubesAppIntents/PostImageIntent.swift index 407e3f75..ab27e1dd 100644 --- a/IceCubesAppIntents/PostImageIntent.swift +++ b/IceCubesAppIntents/PostImageIntent.swift @@ -1,21 +1,20 @@ -import Foundation import AppIntents +import Foundation struct PostImageIntent: AppIntent { static let title: LocalizedStringResource = "Post an image to Mastodon" static var description: IntentDescription { - get { - "Use Ice Cubes to post a status with an image to Mastodon" - } + "Use Ice Cubes to post a status with an image to Mastodon" } + static let openAppWhenRun: Bool = true - + @Parameter(title: "Image", description: "Image to post on Mastodon", supportedTypeIdentifiers: ["public.image"], inputConnectionBehavior: .connectToPreviousIntentResult) var images: [IntentFile]? - + func perform() async throws -> some IntentResult { AppIntentService.shared.handledIntent = .init(intent: self) return .result() diff --git a/IceCubesAppIntents/PostIntent.swift b/IceCubesAppIntents/PostIntent.swift index 59f62270..ebb3ff63 100644 --- a/IceCubesAppIntents/PostIntent.swift +++ b/IceCubesAppIntents/PostIntent.swift @@ -1,18 +1,17 @@ -import Foundation import AppIntents +import Foundation struct PostIntent: AppIntent { static let title: LocalizedStringResource = "Post status to Mastodon" static var description: IntentDescription { - get { - "Use Ice Cubes to post a status to Mastodon" - } + "Use Ice Cubes to post a status to Mastodon" } + static let openAppWhenRun: Bool = true - + @Parameter(title: "Post content", inputConnectionBehavior: .connectToPreviousIntentResult) var content: String? - + func perform() async throws -> some IntentResult { AppIntentService.shared.handledIntent = .init(intent: self) return .result() diff --git a/IceCubesAppIntents/TabIntent.swift b/IceCubesAppIntents/TabIntent.swift index 77efa0e3..61cc415e 100644 --- a/IceCubesAppIntents/TabIntent.swift +++ b/IceCubesAppIntents/TabIntent.swift @@ -1,5 +1,5 @@ -import Foundation import AppIntents +import Foundation enum TabEnum: String, AppEnum, Sendable { case timeline, notifications, mentions, explore, messages, settings @@ -12,13 +12,11 @@ enum TabEnum: String, AppEnum, Sendable { case lists case links - static var typeDisplayName: LocalizedStringResource { - get { "Tab" } - } - + static var typeDisplayName: LocalizedStringResource { "Tab" } + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Tab" - - nonisolated static var caseDisplayRepresentations: [TabEnum : DisplayRepresentation] { + + nonisolated static var caseDisplayRepresentations: [TabEnum: DisplayRepresentation] { [.timeline: .init(title: "Home Timeline"), .trending: .init(title: "Trending Timeline"), .federated: .init(title: "Federated Timeline"), @@ -34,44 +32,43 @@ enum TabEnum: String, AppEnum, Sendable { .followedTags: .init(title: "Followed Tags"), .lists: .init(title: "Lists"), .links: .init(title: "Trending Links"), - .post: .init(title: "New post"), - ] + .post: .init(title: "New post")] } - + var toAppTab: Tab { switch self { case .timeline: - .timeline + .timeline case .notifications: - .notifications + .notifications case .mentions: - .mentions + .mentions case .explore: - .explore + .explore case .messages: - .messages + .messages case .settings: - .settings + .settings case .trending: - .trending + .trending case .federated: - .federated + .federated case .local: - .local + .local case .profile: - .profile + .profile case .bookmarks: - .bookmarks + .bookmarks case .favorites: - .favorites + .favorites case .post: - .post + .post case .followedTags: - .followedTags + .followedTags case .lists: - .lists + .lists case .links: - .links + .links } } } @@ -79,15 +76,14 @@ enum TabEnum: String, AppEnum, Sendable { struct TabIntent: AppIntent { static let title: LocalizedStringResource = "Open on a tab" static var description: IntentDescription { - get { - "Open the app on a specific tab" - } + "Open the app on a specific tab" } + static let openAppWhenRun: Bool = true - + @Parameter(title: "Selected tab") var tab: TabEnum - + @MainActor func perform() async throws -> some IntentResult { AppIntentService.shared.handledIntent = .init(intent: self) diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index b202f4c4..1669e90b 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -326,20 +326,19 @@ public struct AccountDetailView: View { if let account = viewModel.account { Divider() - + Button { routerPath.navigate(to: .blockedAccounts) } label: { Label("account.blocked", systemImage: "person.crop.circle.badge.xmark") } - Button { routerPath.navigate(to: .mutedAccounts) } label: { Label("account.muted", systemImage: "person.crop.circle.badge.moon") } - + Divider() Button { diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift index 72d0537c..ec1a106c 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift @@ -89,10 +89,10 @@ public enum AccountsListMode { case let .accountsList(accounts): self.accounts = accounts link = nil - + case .blocked: (accounts, link) = try await client.getWithLink(endpoint: Accounts.blockList) - + case .muted: (accounts, link) = try await client.getWithLink(endpoint: Accounts.muteList) } @@ -125,14 +125,14 @@ public enum AccountsListMode { case .accountsList: newAccounts = [] link = nil - + case .blocked: (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.blockList) - + case .muted: (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.muteList) } - + accounts.append(contentsOf: newAccounts) let newRelationships: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map(\.id))) diff --git a/Packages/DesignSystem/Sources/DesignSystem/ToolbarItem/CloseToolbarItem.swift b/Packages/DesignSystem/Sources/DesignSystem/ToolbarItem/CloseToolbarItem.swift index 7009710c..36c4bd09 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/ToolbarItem/CloseToolbarItem.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/ToolbarItem/CloseToolbarItem.swift @@ -10,7 +10,7 @@ public struct CloseToolbarItem: ToolbarContent { Button(action: { dismiss() }, label: { - Image(systemName: "xmark.circle") + Image(systemName: "xmark.circle") }) .keyboardShortcut(.cancelAction) } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift index f2d87fc3..f0fcf639 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift @@ -4,9 +4,9 @@ public struct ErrorView: View { public let title: LocalizedStringKey public let message: LocalizedStringKey public let buttonTitle: LocalizedStringKey - public let onButtonPress: (() async -> Void) + public let onButtonPress: () async -> Void - public init(title: LocalizedStringKey, message: LocalizedStringKey, buttonTitle: LocalizedStringKey, onButtonPress: @escaping (() async -> Void) ) { + public init(title: LocalizedStringKey, message: LocalizedStringKey, buttonTitle: LocalizedStringKey, onButtonPress: @escaping (() async -> Void)) { self.title = title self.message = message self.buttonTitle = buttonTitle diff --git a/Packages/Env/Sources/Env/CurrentInstance.swift b/Packages/Env/Sources/Env/CurrentInstance.swift index 1f1a63ef..df80cc14 100644 --- a/Packages/Env/Sources/Env/CurrentInstance.swift +++ b/Packages/Env/Sources/Env/CurrentInstance.swift @@ -34,7 +34,7 @@ import Observation public var isEditAltTextSupported: Bool { version >= 4.1 } - + public var isNotificationsFilterSupported: Bool { version >= 4.3 } diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index c0cc64a1..b0e32513 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -81,7 +81,7 @@ public enum SheetDestination: Identifiable, Hashable { public var id: String { switch self { case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor, - .mentionStatusEditor, .quoteLinkStatusEditor, .prefilledStatusEditor, .imageURL: + .mentionStatusEditor, .quoteLinkStatusEditor, .prefilledStatusEditor, .imageURL: "statusEditor" case .listCreate: "listCreate" @@ -177,9 +177,10 @@ public enum SheetDestination: Identifiable, Hashable { } return .handled } else if let client, - client.isAuth, - client.hasConnection(with: url), - let id = Int(url.lastPathComponent) { + client.isAuth, + client.hasConnection(with: url), + let id = Int(url.lastPathComponent) + { if url.absoluteString.contains(client.server) { navigate(to: .statusDetail(id: String(id))) } else { @@ -189,11 +190,12 @@ public enum SheetDestination: Identifiable, Hashable { } return urlHandler?(url) ?? .systemAction } - + public func handleDeepLink(url: URL) -> OpenURLAction.Result { guard let client, client.isAuth, - let id = Int(url.lastPathComponent) else { + let id = Int(url.lastPathComponent) + else { return urlHandler?(url) ?? .systemAction } // First check whether we already know that the client's server federates with the server this post is on @@ -211,18 +213,18 @@ public enum SheetDestination: Identifiable, Hashable { handlerOrDefault(url: url) return } - + guard client.hasConnection(with: url) else { handlerOrDefault(url: url) return } - + navigateToStatus(url: url, id: id) } - + return .handled } - + private func navigateToStatus(url: URL, id: Int) { guard let client else { return } if url.absoluteString.contains(client.server) { @@ -231,7 +233,7 @@ public enum SheetDestination: Identifiable, Hashable { navigate(to: .remoteStatusDetail(url: url)) } } - + private func handlerOrDefault(url: URL) { if let urlHandler { _ = urlHandler(url) diff --git a/Packages/Models/Sources/Models/NotificationsPolicy.swift b/Packages/Models/Sources/Models/NotificationsPolicy.swift index d87942cd..25a120a0 100644 --- a/Packages/Models/Sources/Models/NotificationsPolicy.swift +++ b/Packages/Models/Sources/Models/NotificationsPolicy.swift @@ -6,7 +6,7 @@ public struct NotificationsPolicy: Codable, Sendable { public var filterNewAccounts: Bool public var filterPrivateMentions: Bool public let summary: Summary - + public struct Summary: Codable, Sendable { public let pendingRequestsCount: String public let pendingNotificationsCount: String diff --git a/Packages/Network/Sources/Network/Endpoint/Notifications.swift b/Packages/Network/Sources/Network/Endpoint/Notifications.swift index 85b7d332..85e61f92 100644 --- a/Packages/Network/Sources/Network/Endpoint/Notifications.swift +++ b/Packages/Network/Sources/Network/Endpoint/Notifications.swift @@ -33,7 +33,7 @@ public enum Notifications: Endpoint { "notifications/clear" } } - + public var jsonValue: (any Encodable)? { switch self { case let .putPolicy(policy): diff --git a/Packages/Notifications/Sources/Notifications/NotificationsHeaderFilteredView.swift b/Packages/Notifications/Sources/Notifications/NotificationsHeaderFilteredView.swift index 84904272..6cd47e53 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsHeaderFilteredView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsHeaderFilteredView.swift @@ -1,14 +1,14 @@ -import SwiftUI -import Models import DesignSystem import Env +import Models +import SwiftUI struct NotificationsHeaderFilteredView: View { @Environment(Theme.self) private var theme @Environment(RouterPath.self) private var routerPath - + let filteredNotifications: NotificationsPolicy.Summary - + var body: some View { if let count = Int(filteredNotifications.pendingNotificationsCount), count > 0 { HStack { diff --git a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift index 49c3f2d4..a90f82f2 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift @@ -7,14 +7,14 @@ import SwiftUI @MainActor public struct NotificationsListView: View { @Environment(\.scenePhase) private var scenePhase - + @Environment(Theme.self) private var theme @Environment(StreamWatcher.self) private var watcher @Environment(Client.self) private var client @Environment(RouterPath.self) private var routerPath @Environment(CurrentAccount.self) private var account @Environment(CurrentInstance.self) private var currentInstance - + @State private var viewModel = NotificationsViewModel() @State private var isNotificationsPolicyPresented: Bool = false @Binding var scrollToTopSignal: Int @@ -24,7 +24,8 @@ public struct NotificationsListView: View { public init(lockedType: Models.Notification.NotificationType? = nil, lockedAccountId: String? = nil, - scrollToTopSignal: Binding) { + scrollToTopSignal: Binding) + { self.lockedType = lockedType self.lockedAccountId = lockedAccountId _scrollToTopSignal = scrollToTopSignal @@ -113,7 +114,7 @@ public struct NotificationsListView: View { .scrollContentBackground(.hidden) .background(theme.primaryBackgroundColor) #endif - .onAppear { + .onAppear { viewModel.client = client viewModel.currentAccount = account if let lockedType { diff --git a/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift b/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift index c9c61d79..730c2cbb 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift @@ -1,50 +1,50 @@ -import SwiftUI -import Network import DesignSystem import Models +import Network +import SwiftUI @MainActor struct NotificationsPolicyView: View { @Environment(\.dismiss) private var dismiss - + @Environment(Client.self) private var client @Environment(Theme.self) private var theme - + @State private var policy: NotificationsPolicy? @State private var isUpdating: Bool = false - + var body: some View { NavigationStack { Form { Section("notifications.content-filter.title-inline") { Toggle(isOn: .init(get: { policy?.filterNotFollowing == true }, set: { newValue in - policy?.filterNotFollowing = newValue - Task { await updatePolicy() } - }), label: { - Text("notifications.content-filter.peopleYouDontFollow") - }) + policy?.filterNotFollowing = newValue + Task { await updatePolicy() } + }), label: { + Text("notifications.content-filter.peopleYouDontFollow") + }) Toggle(isOn: .init(get: { policy?.filterNotFollowers == true }, set: { newValue in - policy?.filterNotFollowers = newValue - Task { await updatePolicy() } - }), label: { - Text("notifications.content-filter.peopleNotFollowingYou") - }) + policy?.filterNotFollowers = newValue + Task { await updatePolicy() } + }), label: { + Text("notifications.content-filter.peopleNotFollowingYou") + }) Toggle(isOn: .init(get: { policy?.filterNewAccounts == true }, set: { newValue in - policy?.filterNewAccounts = newValue - Task { await updatePolicy() } - }), label: { - Text("notifications.content-filter.newAccounts") - }) + policy?.filterNewAccounts = newValue + Task { await updatePolicy() } + }), label: { + Text("notifications.content-filter.newAccounts") + }) Toggle(isOn: .init(get: { policy?.filterPrivateMentions == true }, set: { newValue in - policy?.filterPrivateMentions = newValue - Task { await updatePolicy() } - }), label: { - Text("notifications.content-filter.privateMentions") - }) + policy?.filterPrivateMentions = newValue + Task { await updatePolicy() } + }), label: { + Text("notifications.content-filter.privateMentions") + }) } #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) @@ -63,7 +63,7 @@ struct NotificationsPolicyView: View { .presentationDetents([.medium]) .presentationBackground(.thinMaterial) } - + private func getPolicy() async { defer { isUpdating = false @@ -75,7 +75,7 @@ struct NotificationsPolicyView: View { dismiss() } } - + private func updatePolicy() async { if let policy { defer { @@ -84,7 +84,7 @@ struct NotificationsPolicyView: View { do { isUpdating = true self.policy = try await client.put(endpoint: Notifications.putPolicy(policy: policy)) - } catch { } + } catch {} } } } diff --git a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift index 669e8a15..5197e413 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift @@ -39,7 +39,7 @@ import SwiftUI private let filterKey = "notification-filter" var state: State = .loading var isLockedType: Bool = false - var lockedAccountId: String? = nil + var lockedAccountId: String? var policy: Models.NotificationsPolicy? var selectedType: Models.Notification.NotificationType? { didSet { @@ -156,7 +156,7 @@ import SwiftUI let newNotifications: [Models.Notification] if let lockedAccountId { newNotifications = - try await client.get(endpoint: Notifications.notificationsForAccount(accountId: lockedAccountId, maxId: lastId)) + try await client.get(endpoint: Notifications.notificationsForAccount(accountId: lockedAccountId, maxId: lastId)) } else { newNotifications = try await client.get(endpoint: Notifications.notifications(minId: nil, @@ -180,7 +180,7 @@ import SwiftUI } catch {} } } - + func fetchPolicy() async { policy = try? await client?.get(endpoint: Notifications.policy) } diff --git a/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift b/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift index 6eb91f5b..65ddb46c 100644 --- a/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift +++ b/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift @@ -1,30 +1,31 @@ -import SwiftUI -import Network -import Models import DesignSystem +import Models +import Network +import SwiftUI @MainActor public struct NotificationsRequestsListView: View { @Environment(Client.self) private var client @Environment(Theme.self) private var theme - + enum ViewState { case loading case error case requests(_ data: [NotificationsRequest]) } + @State private var viewState: ViewState = .loading - - public init() { } - + + public init() {} + public var body: some View { List { switch viewState { case .loading: ProgressView() - #if !os(visionOS) + #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) - #endif + #endif .listSectionSeparator(.hidden) case .error: ErrorView(title: "notifications.error.title", @@ -33,10 +34,10 @@ public struct NotificationsRequestsListView: View { { await fetchRequests() } - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif - .listSectionSeparator(.hidden) + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif + .listSectionSeparator(.hidden) case let .requests(data): ForEach(data) { request in NotificationsRequestsRowView(request: request) @@ -46,7 +47,7 @@ public struct NotificationsRequestsListView: View { } label: { Label("account.follow-request.accept", systemImage: "checkmark") } - + Button { Task { await dismissRequest(request) } } label: { @@ -59,32 +60,32 @@ public struct NotificationsRequestsListView: View { } .listStyle(.plain) #if !os(visionOS) - .scrollContentBackground(.hidden) - .background(theme.primaryBackgroundColor) + .scrollContentBackground(.hidden) + .background(theme.primaryBackgroundColor) #endif - .navigationTitle("notifications.content-filter.requests.title") - .navigationBarTitleDisplayMode(.inline) - .task { - await fetchRequests() - } - .refreshable { - await fetchRequests() - } + .navigationTitle("notifications.content-filter.requests.title") + .navigationBarTitleDisplayMode(.inline) + .task { + await fetchRequests() + } + .refreshable { + await fetchRequests() + } } - + private func fetchRequests() async { do { - viewState = .requests(try await client.get(endpoint: Notifications.requests)) + viewState = try .requests(await client.get(endpoint: Notifications.requests)) } catch { viewState = .error } } - + private func acceptRequest(_ request: NotificationsRequest) async { _ = try? await client.post(endpoint: Notifications.acceptRequest(id: request.id)) await fetchRequests() } - + private func dismissRequest(_ request: NotificationsRequest) async { _ = try? await client.post(endpoint: Notifications.dismissRequest(id: request.id)) await fetchRequests() diff --git a/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsRowView.swift b/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsRowView.swift index 424668ca..db16a6bc 100644 --- a/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsRowView.swift +++ b/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsRowView.swift @@ -1,20 +1,20 @@ -import SwiftUI -import Models import DesignSystem import Env +import Models import Network +import SwiftUI struct NotificationsRequestsRowView: View { @Environment(Theme.self) private var theme @Environment(RouterPath.self) private var routerPath @Environment(Client.self) private var client - + let request: NotificationsRequest - + var body: some View { HStack(alignment: .center, spacing: 8) { AvatarView(request.account.avatar, config: .embed) - + VStack(alignment: .leading) { EmojiTextApp(request.account.cachedDisplayName, emojis: request.account.emojis) .font(.scaledBody) @@ -35,14 +35,14 @@ struct NotificationsRequestsRowView: View { .padding(8) .background(.secondary) .clipShape(Circle()) - + Image(systemName: "chevron.right") .foregroundStyle(.secondary) } .onTapGesture { routerPath.navigate(to: .notificationForAccount(accountId: request.account.id)) } - .listRowInsets(.init(top: 12, + .listRowInsets(.init(top: 12, leading: .layoutPadding, bottom: 12, trailing: .layoutPadding)) @@ -50,7 +50,7 @@ struct NotificationsRequestsRowView: View { .listRowBackground(RoundedRectangle(cornerRadius: 8) .foregroundStyle(.background)) #else - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/CustomEmojisView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/CustomEmojisView.swift index 4f654b9b..7dadc5b0 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/CustomEmojisView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/CustomEmojisView.swift @@ -8,11 +8,11 @@ extension StatusEditor { @MainActor struct CustomEmojisView: View { @Environment(\.dismiss) private var dismiss - + @Environment(Theme.self) private var theme - + var viewModel: ViewModel - + var body: some View { NavigationStack { ScrollView { diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/MainView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/MainView.swift index f7366a2f..3fdb15ca 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/MainView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/MainView.swift @@ -16,30 +16,30 @@ public extension StatusEditor { @Environment(AppAccountsManager.self) private var appAccounts @Environment(CurrentAccount.self) private var currentAccount @Environment(Theme.self) private var theme - + @State private var presentationDetent: PresentationDetent = .large @State private var mainSEVM: ViewModel @State private var followUpSEVMs: [ViewModel] = [] @State private var editingMediaContainer: MediaContainer? @State private var scrollID: UUID? - + @FocusState private var editorFocusState: EditorFocusState? - + private var focusedSEVM: ViewModel { if case let .followUp(id) = editorFocusState, let sevm = followUpSEVMs.first(where: { $0.id == id }) { return sevm } - + return mainSEVM } - + public init(mode: ViewModel.Mode) { _mainSEVM = State(initialValue: ViewModel(mode: mode)) } - + public var body: some View { @Bindable var focusedSEVM = focusedSEVM - + NavigationStack { ZStack(alignment: .top) { ScrollView { @@ -53,10 +53,10 @@ public extension StatusEditor { isMain: true ) .id(mainSEVM.id) - + ForEach(followUpSEVMs) { sevm in @Bindable var sevm: ViewModel = sevm - + EditorView( viewModel: sevm, followUpSEVMs: $followUpSEVMs, @@ -73,74 +73,74 @@ public extension StatusEditor { .scrollPosition(id: $scrollID, anchor: .top) .animation(.bouncy(duration: 0.3), value: editorFocusState) .animation(.bouncy(duration: 0.3), value: followUpSEVMs) -#if !os(visionOS) - .background(theme.primaryBackgroundColor) -#endif - .safeAreaInset(edge: .bottom) { - AutoCompleteView(viewModel: focusedSEVM) - } -#if os(visionOS) - .ornament(attachmentAnchor: .scene(.leading)) { - AccessoryView(focusedSEVM: focusedSEVM, - followUpSEVMs: $followUpSEVMs) - } -#else - .safeAreaInset(edge: .bottom) { - if presentationDetent == .large || presentationDetent == .medium { + #if !os(visionOS) + .background(theme.primaryBackgroundColor) + #endif + .safeAreaInset(edge: .bottom) { + AutoCompleteView(viewModel: focusedSEVM) + } + #if os(visionOS) + .ornament(attachmentAnchor: .scene(.leading)) { AccessoryView(focusedSEVM: focusedSEVM, followUpSEVMs: $followUpSEVMs) } - } -#endif - .accessibilitySortPriority(1) // Ensure that all elements inside the `ScrollView` occur earlier than the accessory views - .navigationTitle(focusedSEVM.mode.title) - .navigationBarTitleDisplayMode(.inline) - .toolbar { ToolbarItems(mainSEVM: mainSEVM, - focusedSEVM: focusedSEVM, - followUpSEVMs: followUpSEVMs) } - .toolbarBackground(.visible, for: .navigationBar) - .alert( - "status.error.posting.title", - isPresented: $focusedSEVM.showPostingErrorAlert, - actions: { - Button("OK") {} - }, message: { - Text(mainSEVM.postingError ?? "") - } - ) - .interactiveDismissDisabled(mainSEVM.shouldDisplayDismissWarning) - .onChange(of: appAccounts.currentClient) { _, newValue in - if mainSEVM.mode.isInShareExtension { - currentAccount.setClient(client: newValue) - mainSEVM.client = newValue - for post in followUpSEVMs { - post.client = newValue - } - } - } - .onDrop(of: [.image, .video, .gif, .mpeg4Movie, .quickTimeMovie, .movie], - delegate: focusedSEVM) - .onChange(of: currentAccount.account?.id) { - mainSEVM.currentAccount = currentAccount.account - for p in followUpSEVMs { - p.currentAccount = mainSEVM.currentAccount - } - } - .onChange(of: mainSEVM.visibility) { - for p in followUpSEVMs { - p.visibility = mainSEVM.visibility - } - } - .onChange(of: followUpSEVMs.count) { oldValue, newValue in - if oldValue < newValue { - Task { - try? await Task.sleep(for: .seconds(0.1)) - withAnimation(.bouncy(duration: 0.5)) { - scrollID = followUpSEVMs.last?.id + #else + .safeAreaInset(edge: .bottom) { + if presentationDetent == .large || presentationDetent == .medium { + AccessoryView(focusedSEVM: focusedSEVM, + followUpSEVMs: $followUpSEVMs) + } + } + #endif + .accessibilitySortPriority(1) // Ensure that all elements inside the `ScrollView` occur earlier than the accessory views + .navigationTitle(focusedSEVM.mode.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { ToolbarItems(mainSEVM: mainSEVM, + focusedSEVM: focusedSEVM, + followUpSEVMs: followUpSEVMs) } + .toolbarBackground(.visible, for: .navigationBar) + .alert( + "status.error.posting.title", + isPresented: $focusedSEVM.showPostingErrorAlert, + actions: { + Button("OK") {} + }, message: { + Text(mainSEVM.postingError ?? "") + } + ) + .interactiveDismissDisabled(mainSEVM.shouldDisplayDismissWarning) + .onChange(of: appAccounts.currentClient) { _, newValue in + if mainSEVM.mode.isInShareExtension { + currentAccount.setClient(client: newValue) + mainSEVM.client = newValue + for post in followUpSEVMs { + post.client = newValue + } + } + } + .onDrop(of: [.image, .video, .gif, .mpeg4Movie, .quickTimeMovie, .movie], + delegate: focusedSEVM) + .onChange(of: currentAccount.account?.id) { + mainSEVM.currentAccount = currentAccount.account + for p in followUpSEVMs { + p.currentAccount = mainSEVM.currentAccount + } + } + .onChange(of: mainSEVM.visibility) { + for p in followUpSEVMs { + p.visibility = mainSEVM.visibility + } + } + .onChange(of: followUpSEVMs.count) { oldValue, newValue in + if oldValue < newValue { + Task { + try? await Task.sleep(for: .seconds(0.1)) + withAnimation(.bouncy(duration: 0.5)) { + scrollID = followUpSEVMs.last?.id + } + } } } - } - } if mainSEVM.isPosting { ProgressView(value: mainSEVM.postingProgress, total: 100.0) } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift index e854f8db..8c049832 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift @@ -313,7 +313,7 @@ public extension StatusEditor { processItemsProvider(items: items) case let .imageURL(urls, visibility): Task { - for container in await Self.makeImageContainer(from: urls) { + for container in await Self.makeImageContainer(from: urls) { prepareToPost(for: container) } } @@ -746,16 +746,16 @@ public extension StatusEditor { error: nil ) } - + private static func makeImageContainer(from urls: [URL]) async -> [MediaContainer] { var containers: [MediaContainer] = [] - + for url in urls { let compressor = Compressor() if let compressedData = await compressor.compressImageFrom(url: url), - let image = UIImage(data: compressedData) { - + let image = UIImage(data: compressedData) + { containers.append(MediaContainer( id: UUID().uuidString, image: image, @@ -766,7 +766,7 @@ public extension StatusEditor { )) } } - + return containers }