diff --git a/IceCubesActionExtension/ActionRequestHandler.swift b/IceCubesActionExtension/ActionRequestHandler.swift index 381b6bd2..2a02c793 100644 --- a/IceCubesActionExtension/ActionRequestHandler.swift +++ b/IceCubesActionExtension/ActionRequestHandler.swift @@ -6,11 +6,10 @@ // import MobileCoreServices -import UIKit -import UniformTypeIdentifiers - import Models import Network +import UIKit +import UniformTypeIdentifiers // Sample code was sending this from a thread to another, let asume @Sendable for this extension NSExtensionContext: @unchecked @retroactive Sendable {} @@ -79,11 +78,17 @@ extension ActionRequestHandler { guard itemProvider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else { continue } - guard let dictionary = try await itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier) as? [String: Any] else { + guard + let dictionary = try await itemProvider.loadItem( + forTypeIdentifier: UTType.propertyList.identifier) as? [String: Any] + else { throw Error.loadedItemHasWrongType } - let input = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! [String: Any]? ?? [:] - guard let absoluteStringUrl = input["url"] as? String, let url = URL(string: absoluteStringUrl) else { + let input = + dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! [String: Any]? ?? [:] + guard let absoluteStringUrl = input["url"] as? String, + let url = URL(string: absoluteStringUrl) + else { throw Error.urlNotFound } return url @@ -96,7 +101,8 @@ extension ActionRequestHandler { private func output(wrapping deeplink: URL) -> [NSExtensionItem] { let results = ["deeplink": deeplink.absoluteString] let dictionary = [NSExtensionJavaScriptFinalizeArgumentKey: results] - let provider = NSItemProvider(item: dictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier) + let provider = NSItemProvider( + item: dictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier) let item = NSExtensionItem() item.attachments = [provider] return [item] diff --git a/IceCubesApp/App/Main/AppView.swift b/IceCubesApp/App/Main/AppView.swift index c220c2f7..bd6916fc 100644 --- a/IceCubesApp/App/Main/AppView.swift +++ b/IceCubesApp/App/Main/AppView.swift @@ -1,6 +1,6 @@ +import AVFoundation import Account import AppAccount -import AVFoundation import DesignSystem import Env import KeychainSwift @@ -32,7 +32,8 @@ struct AppView: View { #if os(visionOS) tabBarView #else - if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac + { sidebarView } else { tabBarView @@ -54,11 +55,15 @@ struct AppView: View { @ViewBuilder var tabBarView: some View { - TabView(selection: .init(get: { - selectedTab - }, set: { newTab in - updateTab(with: newTab) - })) { + TabView( + selection: .init( + get: { + selectedTab + }, + set: { newTab in + updateTab(with: newTab) + }) + ) { ForEach(availableTabs) { tab in tab.makeContentView(selectedTab: $selectedTab) .tabItem { @@ -78,17 +83,19 @@ struct AppView: View { .withSheetDestinations(sheetDestinations: $appRouterPath.presentedSheet) .environment(\.selectedTabScrollToTop, selectedTabScrollToTop) } - + private func updateTab(with newTab: AppTab) { if newTab == .post { #if os(visionOS) - openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)) + openWindow( + value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility) + ) #else appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) #endif return } - + HapticManager.shared.fireHaptic(.tabSelection) SoundEffectManager.shared.playSound(.tabSelection) @@ -100,13 +107,13 @@ struct AppView: View { } else { selectedTabScrollToTop = -1 } - + selectedTab = newTab } private func badgeFor(tab: AppTab) -> Int { if tab == .notifications, selectedTab != tab, - let token = appAccountsManager.currentAccount.oauthToken + let token = appAccountsManager.currentAccount.oauthToken { return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0) } @@ -115,29 +122,32 @@ struct AppView: View { #if !os(visionOS) var sidebarView: some View { - SideBarView(selectedTab: .init(get: { - selectedTab - }, set: { newTab in - updateTab(with: newTab) - }), tabs: availableTabs) - { + SideBarView( + selectedTab: .init( + get: { + selectedTab + }, + set: { newTab in + updateTab(with: newTab) + }), tabs: availableTabs + ) { HStack(spacing: 0) { if #available(iOS 18.0, *) { baseTabView - #if targetEnvironment(macCatalyst) - .tabViewStyle(.sidebarAdaptable) - .introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in - tabview.sidebar.isHidden = true - } - #else - .tabViewStyle(.tabBarOnly) - #endif + #if targetEnvironment(macCatalyst) + .tabViewStyle(.sidebarAdaptable) + .introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in + tabview.sidebar.isHidden = true + } + #else + .tabViewStyle(.tabBarOnly) + #endif } else { baseTabView } if horizontalSizeClass == .regular, - appAccountsManager.currentClient.isAuth, - userPreferences.showiPadSecondaryColumn + appAccountsManager.currentClient.isAuth, + userPreferences.showiPadSecondaryColumn { Divider().edgesIgnoringSafeArea(.all) notificationsSecondaryColumn @@ -148,7 +158,7 @@ struct AppView: View { .environment(\.selectedTabScrollToTop, selectedTabScrollToTop) } #endif - + private var baseTabView: some View { TabView(selection: $selectedTab) { ForEach(availableTabs) { tab in @@ -162,17 +172,16 @@ struct AppView: View { } } #if !os(visionOS) - .introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in - tabview.tabBar.isHidden = horizontalSizeClass == .regular - tabview.customizableViewControllers = [] - tabview.moreNavigationController.isNavigationBarHidden = true - } + .introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in + tabview.tabBar.isHidden = horizontalSizeClass == .regular + tabview.customizableViewControllers = [] + tabview.moreNavigationController.isNavigationBarHidden = true + } #endif } var notificationsSecondaryColumn: some View { - NotificationsTab(selectedTab: .constant(.notifications) - , lockedType: nil) + NotificationsTab(selectedTab: .constant(.notifications), lockedType: nil) .environment(\.isSecondaryColumn, true) .frame(maxWidth: .secondaryColumnWidth) .id(appAccountsManager.currentAccount.id) diff --git a/IceCubesApp/App/Main/IceCubesApp+Menu.swift b/IceCubesApp/App/Main/IceCubesApp+Menu.swift index 953d4e2c..b2e5c09a 100644 --- a/IceCubesApp/App/Main/IceCubesApp+Menu.swift +++ b/IceCubesApp/App/Main/IceCubesApp+Menu.swift @@ -17,9 +17,12 @@ extension IceCubesApp { .keyboardShortcut("n", modifiers: .shift) Button("menu.new-post") { #if targetEnvironment(macCatalyst) - openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)) + openWindow( + value: WindowDestinationEditor.newStatusEditor( + visibility: userPreferences.postVisibility)) #else - appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) + appRouterPath.presentedSheet = .newStatusEditor( + visibility: userPreferences.postVisibility) #endif } .keyboardShortcut("n", modifiers: .command) diff --git a/IceCubesApp/App/Main/IceCubesApp+Scene.swift b/IceCubesApp/App/Main/IceCubesApp+Scene.swift index b8b9da4b..e516e766 100644 --- a/IceCubesApp/App/Main/IceCubesApp+Scene.swift +++ b/IceCubesApp/App/Main/IceCubesApp+Scene.swift @@ -27,15 +27,19 @@ extension IceCubesApp { .environment(\.isSupporter, isSupporter) .sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in if #available(iOS 18.0, *) { - MediaUIView(selectedAttachment: selectedMediaAttachment, - attachments: quickLook.mediaAttachments) + MediaUIView( + selectedAttachment: selectedMediaAttachment, + attachments: quickLook.mediaAttachments + ) .presentationBackground(.ultraThinMaterial) .presentationCornerRadius(16) .presentationSizing(.page) .withEnvironments() } else { - MediaUIView(selectedAttachment: selectedMediaAttachment, - attachments: quickLook.mediaAttachments) + MediaUIView( + selectedAttachment: selectedMediaAttachment, + attachments: quickLook.mediaAttachments + ) .presentationBackground(.ultraThinMaterial) .presentationCornerRadius(16) .withEnvironments() @@ -44,9 +48,11 @@ extension IceCubesApp { .onChange(of: pushNotificationsService.handledNotification) { _, newValue in if newValue != nil { pushNotificationsService.handledNotification = nil - if appAccountsManager.currentAccount.oauthToken?.accessToken != newValue?.account.token.accessToken, - let account = appAccountsManager.availableAccounts.first(where: - { $0.oauthToken?.accessToken == newValue?.account.token.accessToken }) + if appAccountsManager.currentAccount.oauthToken?.accessToken + != newValue?.account.token.accessToken, + let account = appAccountsManager.availableAccounts.first(where: { + $0.oauthToken?.accessToken == newValue?.account.token.accessToken + }) { appAccountsManager.currentAccount = account DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { @@ -79,9 +85,9 @@ extension IceCubesApp { } } #if targetEnvironment(macCatalyst) - .windowResize() + .windowResize() #elseif os(visionOS) - .defaultSize(width: 800, height: 1200) + .defaultSize(width: 800, height: 1200) #endif } @@ -122,8 +128,9 @@ extension IceCubesApp { Group { switch destination.wrappedValue { case let .mediaViewer(attachments, selectedAttachment): - MediaUIView(selectedAttachment: selectedAttachment, - attachments: attachments) + MediaUIView( + selectedAttachment: selectedAttachment, + attachments: attachments) case .none: EmptyView() } @@ -141,19 +148,23 @@ extension IceCubesApp { 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) + appRouterPath.presentedSheet = .imageURL( + urls: urls, + visibility: userPreferences.postVisibility) } } } diff --git a/IceCubesApp/App/Main/IceCubesApp.swift b/IceCubesApp/App/Main/IceCubesApp.swift index a83f844c..acc43356 100644 --- a/IceCubesApp/App/Main/IceCubesApp.swift +++ b/IceCubesApp/App/Main/IceCubesApp.swift @@ -1,6 +1,6 @@ +import AVFoundation import Account import AppAccount -import AVFoundation import DesignSystem import Env import KeychainSwift @@ -36,9 +36,9 @@ struct IceCubesApp: App { init() { #if DEBUG - // Enable "GraphReuseLogging" for debugging purpose - // subsystem: "com.apple.SwiftUI" category: "GraphReuse" - UserDefaults.standard.register(defaults: ["com.apple.SwiftUI.GraphReuseLogging": true]) + // Enable "GraphReuseLogging" for debugging purpose + // subsystem: "com.apple.SwiftUI" category: "GraphReuse" + UserDefaults.standard.register(defaults: ["com.apple.SwiftUI.GraphReuseLogging": true]) #endif } @@ -53,7 +53,8 @@ struct IceCubesApp: App { userPreferences.setClient(client: client) Task { await currentInstance.fetchCurrentInstance() - watcher.setClient(client: client, instanceStreamingURL: currentInstance.instance?.urls?.streamingApi) + watcher.setClient( + client: client, instanceStreamingURL: currentInstance.instance?.urls?.streamingApi) watcher.watch(streams: [.user, .direct]) } } @@ -65,7 +66,8 @@ struct IceCubesApp: App { case .active: watcher.watch(streams: [.user, .direct]) UNUserNotificationCenter.current().setBadgeCount(0) - userPreferences.reloadNotificationsCount(tokens: appAccountsManager.availableAccounts.compactMap(\.oauthToken)) + userPreferences.reloadNotificationsCount( + tokens: appAccountsManager.availableAccounts.compactMap(\.oauthToken)) Task { await userPreferences.refreshServerPreferences() } @@ -90,9 +92,10 @@ struct IceCubesApp: App { } class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_: UIApplication, - didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool - { + func application( + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers) try? AVAudioSession.sharedInstance().setActive(true) PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts) @@ -102,9 +105,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - func application(_: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) - { + func application( + _: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { PushNotificationsService.shared.pushToken = deviceToken Task { PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts) @@ -114,12 +118,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {} - func application(_: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any]) async -> UIBackgroundFetchResult { - UserPreferences.shared.reloadNotificationsCount(tokens: AppAccountsManager.shared.availableAccounts.compactMap(\.oauthToken)) + func application(_: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any]) async + -> UIBackgroundFetchResult + { + UserPreferences.shared.reloadNotificationsCount( + tokens: AppAccountsManager.shared.availableAccounts.compactMap(\.oauthToken)) return .noData } - func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { + func application( + _: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, + options _: UIScene.ConnectionOptions + ) -> UISceneConfiguration { let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) if connectingSceneSession.role == .windowApplication { configuration.delegateClass = SceneDelegate.self diff --git a/IceCubesApp/App/Report/ReportView.swift b/IceCubesApp/App/Report/ReportView.swift index dcb37d9a..72f26788 100644 --- a/IceCubesApp/App/Report/ReportView.swift +++ b/IceCubesApp/App/Report/ReportView.swift @@ -23,9 +23,10 @@ public struct ReportView: View { NavigationStack { Form { Section { - TextField("report.comment.placeholder", - text: $commentText, - axis: .vertical) + TextField( + "report.comment.placeholder", + text: $commentText, + axis: .vertical) } .listRowBackground(theme.primaryBackgroundColor) @@ -40,33 +41,35 @@ public struct ReportView: View { .background(theme.secondaryBackgroundColor) .scrollDismissesKeyboard(.immediately) #endif - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - isSendingReport = true - Task { - do { - let _: ReportSent = - try await client.post(endpoint: Statuses.report(accountId: status.account.id, - statusId: status.id, - comment: commentText)) - dismiss() - isSendingReport = false - } catch { - isSendingReport = false - } - } - } label: { - if isSendingReport { - ProgressView() - } else { - Text("report.action.send") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + isSendingReport = true + Task { + do { + let _: ReportSent = + try await client.post( + endpoint: Statuses.report( + accountId: status.account.id, + statusId: status.id, + comment: commentText)) + dismiss() + isSendingReport = false + } catch { + isSendingReport = false } } + } label: { + if isSendingReport { + ProgressView() + } else { + Text("report.action.send") + } } - - CancelToolbarItem() } + + CancelToolbarItem() + } } } } diff --git a/IceCubesApp/App/SafariRouter.swift b/IceCubesApp/App/SafariRouter.swift index 0cc3cd98..dce732bb 100644 --- a/IceCubesApp/App/SafariRouter.swift +++ b/IceCubesApp/App/SafariRouter.swift @@ -1,10 +1,10 @@ +import AppAccount import DesignSystem import Env import Models import Observation import SafariServices import SwiftUI -import AppAccount import WebKit extension View { @@ -27,21 +27,25 @@ private struct SafariRouter: ViewModifier { func body(content: Content) -> some View { content - .environment(\.openURL, OpenURLAction { url in - // Open internal URL. - guard !isSecondaryColumn else { return .discarded } - return routerPath.handle(url: url) - }) + .environment( + \.openURL, + OpenURLAction { url in + // Open internal URL. + guard !isSecondaryColumn else { return .discarded } + return routerPath.handle(url: url) + } + ) .onOpenURL { url in // Open external URL (from icecubesapp://) guard !isSecondaryColumn else { return } if url.absoluteString == "icecubesapp://subclub" { #if !os(visionOS) - safariManager.dismiss() + safariManager.dismiss() #endif return } - let urlString = url.absoluteString.replacingOccurrences(of: AppInfo.scheme, with: "https://") + let urlString = url.absoluteString.replacingOccurrences( + of: AppInfo.scheme, with: "https://") guard let url = URL(string: urlString), url.host != nil else { return } _ = routerPath.handleDeepLink(url: url) } @@ -57,17 +61,18 @@ private struct SafariRouter: ViewModifier { return .handled } } else if url.query()?.contains("callback=") == false, - url.host() == AppInfo.premiumInstance, - let accountName = appAccount.currentAccount.accountName { + url.host() == AppInfo.premiumInstance, + let accountName = appAccount.currentAccount.accountName + { let newURL = url.appending(queryItems: [ .init(name: "callback", value: "icecubesapp://subclub"), - .init(name: "id", value: "@\(accountName)") + .init(name: "id", value: "@\(accountName)"), ]) - + #if !os(visionOS) - return safariManager.open(newURL) + return safariManager.open(newURL) #else - return .systemAction + return .systemAction #endif } #if !targetEnvironment(macCatalyst) @@ -86,13 +91,13 @@ private struct SafariRouter: ViewModifier { #endif } } - #if !os(visionOS) - .background { - WindowReader { window in - safariManager.windowScene = window.windowScene + #if !os(visionOS) + .background { + WindowReader { window in + safariManager.windowScene = window.windowScene + } } - } - #endif + #endif } } @@ -123,7 +128,7 @@ private struct SafariRouter: ViewModifier { return .handled } - + func dismiss() { viewController.presentedViewController?.dismiss(animated: true) window?.resignKey() diff --git a/IceCubesApp/App/SideBarView.swift b/IceCubesApp/App/SideBarView.swift index 5e4ea7ef..d19373f7 100644 --- a/IceCubesApp/App/SideBarView.swift +++ b/IceCubesApp/App/SideBarView.swift @@ -26,7 +26,7 @@ struct SideBarView: View { private func badgeFor(tab: AppTab) -> Int { if tab == .notifications, selectedTab != tab, - let token = appAccounts.currentAccount.oauthToken + let token = appAccounts.currentAccount.oauthToken { return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0) } @@ -36,8 +36,9 @@ struct SideBarView: View { private func makeIconForTab(tab: AppTab) -> some View { ZStack(alignment: .topTrailing) { HStack { - SideBarIcon(systemIconName: tab.iconName, - isSelected: tab == selectedTab) + SideBarIcon( + systemIconName: tab.iconName, + isSelected: tab == selectedTab) if userPreferences.isSidebarExpanded { Text(tab.title) .font(.headline) @@ -45,14 +46,19 @@ struct SideBarView: View { .frame(maxWidth: .infinity, alignment: .leading) } } - .frame(width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24, height: 50) - .background(tab == selectedTab ? theme.primaryBackgroundColor : .clear, - in: RoundedRectangle(cornerRadius: 8)) + .frame( + width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24, + height: 50 + ) + .background( + tab == selectedTab ? theme.primaryBackgroundColor : .clear, + in: RoundedRectangle(cornerRadius: 8) + ) .cornerRadius(8) .shadow(color: tab == selectedTab ? .black.opacity(0.2) : .clear, radius: 5) .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(tab == selectedTab ? theme.labelColor.opacity(0.1) : .clear, lineWidth: 1) + RoundedRectangle(cornerRadius: 8) + .stroke(tab == selectedTab ? theme.labelColor.opacity(0.1) : .clear, lineWidth: 1) ) let badge = badgeFor(tab: tab) if badge > 0 { @@ -76,7 +82,9 @@ struct SideBarView: View { private var postButton: some View { Button { #if targetEnvironment(macCatalyst) || os(visionOS) - openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)) + openWindow( + value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility) + ) #else routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) #endif @@ -106,21 +114,25 @@ struct SideBarView: View { } label: { ZStack(alignment: .topTrailing) { if userPreferences.isSidebarExpanded { - AppAccountView(viewModel: .init(appAccount: account, - isCompact: false, - isInSettings: false), - isParentPresented: .constant(false)) + AppAccountView( + viewModel: .init( + appAccount: account, + isCompact: false, + isInSettings: false), + isParentPresented: .constant(false)) } else { - AppAccountView(viewModel: .init(appAccount: account, - isCompact: true, - isInSettings: false), - isParentPresented: .constant(false)) + AppAccountView( + viewModel: .init( + appAccount: account, + isCompact: true, + isInSettings: false), + isParentPresented: .constant(false)) } if !userPreferences.isSidebarExpanded, - showBadge, - let token = account.oauthToken, - let notificationsCount = userPreferences.notificationsCount[token], - notificationsCount > 0 + showBadge, + let token = account.oauthToken, + let notificationsCount = userPreferences.notificationsCount[token], + notificationsCount > 0 { makeBadgeView(count: notificationsCount) } @@ -128,10 +140,13 @@ struct SideBarView: View { .padding(.leading, userPreferences.isSidebarExpanded ? 16 : 0) } .help(accountButtonTitle(accountName: account.accountName)) - .frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth, height: 50) + .frame( + width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth, height: 50 + ) .padding(.vertical, 8) - .background(selectedTab == .profile && account.id == appAccounts.currentAccount.id ? - theme.secondaryBackgroundColor : .clear) + .background( + selectedTab == .profile && account.id == appAccounts.currentAccount.id + ? theme.secondaryBackgroundColor : .clear) } private func accountButtonTitle(accountName: String?) -> LocalizedStringKey { @@ -174,8 +189,9 @@ struct SideBarView: View { tabsView } else { ForEach(appAccounts.availableAccounts) { account in - makeAccountButton(account: account, - showBadge: account.id != appAccounts.currentAccount.id) + makeAccountButton( + account: account, + showBadge: account.id != appAccounts.currentAccount.id) if account.id == appAccounts.currentAccount.id { tabsView } @@ -186,21 +202,23 @@ struct SideBarView: View { .frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) .scrollContentBackground(.hidden) .background(.thinMaterial) - .safeAreaInset(edge: .bottom, content: { - HStack(spacing: 16) { - postButton - .padding(.vertical, 24) - .padding(.leading, userPreferences.isSidebarExpanded ? 18 : 0) - if userPreferences.isSidebarExpanded { - Text("menu.new-post") - .font(.subheadline) - .foregroundColor(theme.labelColor) - .frame(maxWidth: .infinity, alignment: .leading) + .safeAreaInset( + edge: .bottom, + content: { + HStack(spacing: 16) { + postButton + .padding(.vertical, 24) + .padding(.leading, userPreferences.isSidebarExpanded ? 18 : 0) + if userPreferences.isSidebarExpanded { + Text("menu.new-post") + .font(.subheadline) + .foregroundColor(theme.labelColor) + .frame(maxWidth: .infinity, alignment: .leading) + } } - } - .frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - .background(.thinMaterial) - }) + .frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) + .background(.thinMaterial) + }) Divider().edgesIgnoringSafeArea(.all) } content() diff --git a/IceCubesApp/App/Tabs/NotificationTab.swift b/IceCubesApp/App/Tabs/NotificationTab.swift index 963cad49..03468a7c 100644 --- a/IceCubesApp/App/Tabs/NotificationTab.swift +++ b/IceCubesApp/App/Tabs/NotificationTab.swift @@ -57,7 +57,8 @@ struct NotificationsTab: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { switch type { case .follow, .follow_request: - routerPath.navigate(to: .accountDetailWithAccount(account: newValue.notification.account)) + routerPath.navigate( + to: .accountDetailWithAccount(account: newValue.notification.account)) default: if let status = newValue.notification.status { routerPath.navigate(to: .statusDetailWithStatus(status: status)) @@ -81,7 +82,9 @@ struct NotificationsTab: View { private func clearNotifications() { if selectedTab == .notifications || isSecondaryColumn { - if let token = appAccount.currentAccount.oauthToken, userPreferences.notificationsCount[token] ?? 0 > 0 { + if let token = appAccount.currentAccount.oauthToken, + userPreferences.notificationsCount[token] ?? 0 > 0 + { userPreferences.notificationsCount[token] = 0 } if watcher.unreadNotificationsCount > 0 { diff --git a/IceCubesApp/App/Tabs/Settings/AboutView.swift b/IceCubesApp/App/Tabs/Settings/AboutView.swift index 5203b984..63ccd55a 100644 --- a/IceCubesApp/App/Tabs/Settings/AboutView.swift +++ b/IceCubesApp/App/Tabs/Settings/AboutView.swift @@ -49,22 +49,26 @@ struct AboutView: View { Spacer() } #endif - Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) { + Link( + destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")! + ) { Label("settings.support.privacy-policy", systemImage: "lock") } - Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")!) { + Link( + destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")! + ) { Label("settings.support.terms-of-use", systemImage: "checkmark.shield") } } footer: { Text("\(versionNumber)© 2024 Thomas Ricouard") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif followAccountsSection - + Section("Telemetry") { Link(destination: .init(string: "https://telemetrydeck.com")!) { Label("Telemetry by TelemetryDeck", systemImage: "link") @@ -74,35 +78,37 @@ struct AboutView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif Section { - Text(""" - • [EmojiText](https://github.com/divadretlaw/EmojiText) + Text( + """ + • [EmojiText](https://github.com/divadretlaw/EmojiText) - • [HTML2Markdown](https://gitlab.com/mflint/HTML2Markdown) + • [HTML2Markdown](https://gitlab.com/mflint/HTML2Markdown) - • [KeychainSwift](https://github.com/evgenyneu/keychain-swift) + • [KeychainSwift](https://github.com/evgenyneu/keychain-swift) - • [LRUCache](https://github.com/nicklockwood/LRUCache) + • [LRUCache](https://github.com/nicklockwood/LRUCache) - • [Bodega](https://github.com/mergesort/Bodega) + • [Bodega](https://github.com/mergesort/Bodega) - • [Nuke](https://github.com/kean/Nuke) + • [Nuke](https://github.com/kean/Nuke) - • [SwiftSoup](https://github.com/scinfu/SwiftSoup.git) + • [SwiftSoup](https://github.com/scinfu/SwiftSoup.git) - • [Atkinson Hyperlegible](https://github.com/googlefonts/atkinson-hyperlegible) + • [Atkinson Hyperlegible](https://github.com/googlefonts/atkinson-hyperlegible) - • [OpenDyslexic](http://opendyslexic.org) + • [OpenDyslexic](http://opendyslexic.org) - • [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect) + • [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect) - • [RevenueCat](https://github.com/RevenueCat/purchases-ios) + • [RevenueCat](https://github.com/RevenueCat/purchases-ios) - • [SFSafeSymbols](https://github.com/SFSafeSymbols/SFSafeSymbols) - """) + • [SFSafeSymbols](https://github.com/SFSafeSymbols/SFSafeSymbols) + """ + ) .multilineTextAlignment(.leading) .font(.scaledSubheadline) .foregroundStyle(.secondary) @@ -111,7 +117,7 @@ struct AboutView: View { .textCase(nil) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .task { @@ -122,9 +128,11 @@ struct AboutView: View { .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) #endif - .navigationTitle(Text("settings.about.title")) - .navigationBarTitleDisplayMode(.large) - .environment(\.openURL, OpenURLAction { url in + .navigationTitle(Text("settings.about.title")) + .navigationBarTitleDisplayMode(.large) + .environment( + \.openURL, + OpenURLAction { url in routerPath.handle(url: url) }) } @@ -137,14 +145,14 @@ struct AboutView: View { AccountsListRow(viewModel: dimillianAccount) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } else { Section { ProgressView() } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } } @@ -152,13 +160,15 @@ struct AboutView: View { private func fetchAccounts() async { await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - let viewModel = try await fetchAccountViewModel(client, account: "dimillian@mastodon.social") + let viewModel = try await fetchAccountViewModel( + client, account: "dimillian@mastodon.social") await MainActor.run { dimillianAccount = viewModel } } group.addTask { - let viewModel = try await fetchAccountViewModel(client, account: "icecubesapp@mastodon.online") + let viewModel = try await fetchAccountViewModel( + client, account: "icecubesapp@mastodon.online") await MainActor.run { iceCubesAccount = viewModel } @@ -166,9 +176,12 @@ struct AboutView: View { } } - private func fetchAccountViewModel(_ client: Client, account: String) async throws -> AccountsListRowViewModel { + private func fetchAccountViewModel(_ client: Client, account: String) async throws + -> AccountsListRowViewModel + { let dimillianAccount: Account = try await client.get(endpoint: Accounts.lookup(name: account)) - let rel: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [dimillianAccount.id])) + let rel: [Relationship] = try await client.get( + endpoint: Accounts.relationships(ids: [dimillianAccount.id])) return .init(account: dimillianAccount, relationShip: rel.first) } } diff --git a/IceCubesApp/App/Tabs/Settings/AccountSettingView.swift b/IceCubesApp/App/Tabs/Settings/AccountSettingView.swift index d93631a5..1d7f5f08 100644 --- a/IceCubesApp/App/Tabs/Settings/AccountSettingView.swift +++ b/IceCubesApp/App/Tabs/Settings/AccountSettingView.swift @@ -47,20 +47,25 @@ struct AccountSettingsView: View { } .buttonStyle(.plain) } - if let subscription = pushNotifications.subscriptions.first(where: { $0.account.token == appAccount.oauthToken }) { + if let subscription = pushNotifications.subscriptions.first(where: { + $0.account.token == appAccount.oauthToken + }) { NavigationLink(destination: PushNotificationsView(subscription: subscription)) { - Label("settings.general.push-notifications", systemImage: "bell.and.waves.left.and.right") + Label( + "settings.general.push-notifications", systemImage: "bell.and.waves.left.and.right") } } } .listRowBackground(theme.primaryBackgroundColor) Section { - Label("settings.account.cached-posts-\(String(cachedPostsCount))", systemImage: "internaldrive") + Label( + "settings.account.cached-posts-\(String(cachedPostsCount))", systemImage: "internaldrive") Button("settings.account.action.delete-cache", role: .destructive) { Task { await timelineCache.clearCache(for: appAccountsManager.currentClient.id) - cachedPostsCount = await timelineCache.cachedPostsCount(for: appAccountsManager.currentClient.id) + cachedPostsCount = await timelineCache.cachedPostsCount( + for: appAccountsManager.currentClient.id) } } } @@ -81,7 +86,9 @@ struct AccountSettingsView: View { Task { let client = Client(server: appAccount.server, oauthToken: token) await timelineCache.clearCache(for: client.id) - if let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) { + if let sub = pushNotifications.subscriptions.first(where: { + $0.account.token == token + }) { await sub.deleteSubscription() } appAccountsManager.delete(account: appAccount) @@ -106,7 +113,8 @@ struct AccountSettingsView: View { } } .task { - cachedPostsCount = await timelineCache.cachedPostsCount(for: appAccountsManager.currentClient.id) + cachedPostsCount = await timelineCache.cachedPostsCount( + for: appAccountsManager.currentClient.id) } .navigationTitle(account.safeDisplayName) #if !os(visionOS) diff --git a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift index 3b8ebf22..f733312f 100644 --- a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift +++ b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift @@ -35,13 +35,14 @@ struct AddAccountView: View { private let instanceNamePublisher = PassthroughSubject() private var sanitizedName: String { - var name = instanceName + var name = + instanceName .replacingOccurrences(of: "http://", with: "") .replacingOccurrences(of: "https://", with: "") if name.contains("@") { let parts = name.components(separatedBy: "@") - name = parts[parts.count - 1] // [@]username@server.address.com + name = parts[parts.count - 1] // [@]username@server.address.com } return name } @@ -56,9 +57,9 @@ struct AddAccountView: View { NavigationStack { Form { TextField("instance.url", text: $instanceName) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif .keyboardType(.URL) .textContentType(.URL) .textInputAutocapitalization(.never) @@ -87,73 +88,73 @@ struct AddAccountView: View { .background(theme.secondaryBackgroundColor) .scrollDismissesKeyboard(.immediately) #endif - .toolbar { - CancelToolbarItem() - } - .onAppear { - isInstanceURLFieldFocused = true - let instanceName = instanceName - Task { - let instances = await instanceSocialClient.fetchInstances(keyword: instanceName) - withAnimation { - self.instances = instances - } + .toolbar { + CancelToolbarItem() + } + .onAppear { + isInstanceURLFieldFocused = true + let instanceName = instanceName + Task { + let instances = await instanceSocialClient.fetchInstances(keyword: instanceName) + withAnimation { + self.instances = instances } - isSigninIn = false } - .onChange(of: instanceName) { - searchingTask.cancel() - let instanceName = instanceName - let instanceSocialClient = instanceSocialClient - searchingTask = Task { - try? await Task.sleep(for: .seconds(0.1)) - guard !Task.isCancelled else { return } + isSigninIn = false + } + .onChange(of: instanceName) { + searchingTask.cancel() + let instanceName = instanceName + let instanceSocialClient = instanceSocialClient + searchingTask = Task { + try? await Task.sleep(for: .seconds(0.1)) + guard !Task.isCancelled else { return } - let instances = await instanceSocialClient.fetchInstances(keyword: instanceName) - withAnimation { - self.instances = instances - } + let instances = await instanceSocialClient.fetchInstances(keyword: instanceName) + withAnimation { + self.instances = instances } + } - getInstanceDetailTask.cancel() - getInstanceDetailTask = Task { - try? await Task.sleep(for: .seconds(0.1)) - guard !Task.isCancelled else { return } + getInstanceDetailTask.cancel() + getInstanceDetailTask = Task { + try? await Task.sleep(for: .seconds(0.1)) + guard !Task.isCancelled else { return } - do { - // bare bones preflight for domain validity - let instanceDetailClient = Client(server: sanitizedName) - if - instanceDetailClient.server.contains("."), - instanceDetailClient.server.last != "." - { - let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance) - withAnimation { - self.instance = instance - self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box - } - instanceFetchError = nil - } else { - instance = nil - instanceFetchError = nil + do { + // bare bones preflight for domain validity + let instanceDetailClient = Client(server: sanitizedName) + if instanceDetailClient.server.contains("."), + instanceDetailClient.server.last != "." + { + let instance: Instance = try await instanceDetailClient.get( + endpoint: Instances.instance) + withAnimation { + self.instance = instance + self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box } - } catch _ as ServerError { - instance = nil - instanceFetchError = "account.add.error.instance-not-supported" - } catch { + instanceFetchError = nil + } else { instance = nil instanceFetchError = nil } + } catch _ as ServerError { + instance = nil + instanceFetchError = "account.add.error.instance-not-supported" + } catch { + instance = nil + instanceFetchError = nil } } - .onChange(of: scenePhase) { _, newValue in - switch newValue { - case .active: - isSigninIn = false - default: - break - } + } + .onChange(of: scenePhase) { _, newValue in + switch newValue { + case .active: + isSigninIn = false + default: + break } + } } } @@ -183,7 +184,7 @@ struct AddAccountView: View { .buttonStyle(.borderedProminent) } #if !os(visionOS) - .listRowBackground(theme.tintColor) + .listRowBackground(theme.tintColor) #endif } @@ -222,20 +223,23 @@ struct AddAccountView: View { .foregroundStyle(theme.tintColor) } .padding(.bottom, 5) - Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "") - .foregroundStyle(Color.secondary) - .lineLimit(10) + Text( + instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) + ?? "" + ) + .foregroundStyle(Color.secondary) + .lineLimit(10) } .font(.scaledFootnote) .padding(10) } } #if !os(visionOS) - .background(theme.primaryBackgroundColor) - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0)) - .listRowSeparator(.hidden) - .clipShape(RoundedRectangle(cornerRadius: 4)) + .background(theme.primaryBackgroundColor) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0)) + .listRowSeparator(.hidden) + .clipShape(RoundedRectangle(cornerRadius: 4)) #endif } } @@ -273,8 +277,9 @@ struct AddAccountView: View { private func signIn() async { signInClient = .init(server: sanitizedName) if let oauthURL = try? await signInClient?.oauthURL(), - let url = try? await webAuthenticationSession.authenticate(using: oauthURL, - callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: "")) + let url = try? await webAuthenticationSession.authenticate( + using: oauthURL, + callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: "")) { await continueSignIn(url: url) } else { @@ -292,9 +297,11 @@ struct AddAccountView: View { let client = Client(server: client.server, oauthToken: oauthToken) let account: Account = try await client.get(endpoint: Accounts.verifyCredentials) Telemetry.signal("account.added") - appAccountsManager.add(account: AppAccount(server: client.server, - accountName: "\(account.acct)@\(client.server)", - oauthToken: oauthToken)) + appAccountsManager.add( + account: AppAccount( + server: client.server, + accountName: "\(account.acct)@\(client.server)", + oauthToken: oauthToken)) Task { pushNotifications.setAccounts(accounts: appAccountsManager.pushAccounts) await pushNotifications.updateSubscriptions(forceCreate: true) diff --git a/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift b/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift index ab561b5e..59d04dfc 100644 --- a/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift @@ -30,11 +30,14 @@ struct ContentSettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif Section("settings.content.sharing") { - Picker("settings.content.sharing.share-button-behavior", selection: $userPreferences.shareButtonBehavior) { + Picker( + "settings.content.sharing.share-button-behavior", + selection: $userPreferences.shareButtonBehavior + ) { ForEach(PreferredShareButtonBehavior.allCases, id: \.rawValue) { option in Text(option.title) .tag(option) @@ -42,7 +45,7 @@ struct ContentSettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif Section("settings.content.instance-settings") { @@ -51,7 +54,7 @@ struct ContentSettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif .onChange(of: userPreferences.useInstanceContentSettings) { _, newVal in if newVal { @@ -85,21 +88,27 @@ struct ContentSettingsView: View { Text("settings.content.collapse-long-posts-hint") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif Section("settings.content.posting") { - Picker("settings.content.default-visibility", selection: $userPreferences.appDefaultPostVisibility) { + Picker( + "settings.content.default-visibility", + selection: $userPreferences.appDefaultPostVisibility + ) { ForEach(Visibility.allCases, id: \.rawValue) { vis in Text(vis.title).tag(vis) } } .disabled(userPreferences.useInstanceContentSettings) - Picker("settings.content.default-reply-visibility", selection: $userPreferences.appDefaultReplyVisibility) { + Picker( + "settings.content.default-reply-visibility", + selection: $userPreferences.appDefaultReplyVisibility + ) { ForEach(Visibility.allCases, id: \.rawValue) { vis in - if UserPreferences.getIntOfVisibility(vis) <= - UserPreferences.getIntOfVisibility(userPreferences.postVisibility) + if UserPreferences.getIntOfVisibility(vis) + <= UserPreferences.getIntOfVisibility(userPreferences.postVisibility) { Text(vis.title).tag(vis) } @@ -119,7 +128,7 @@ struct ContentSettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif Section("timeline.content-filter.title") { @@ -137,7 +146,7 @@ struct ContentSettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .navigationTitle("settings.content.navigation-title") diff --git a/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift b/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift index 9c27e098..ff1b8b9d 100644 --- a/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift @@ -29,9 +29,10 @@ struct DisplaySettingsView: View { @State private var isFontSelectorPresented = false - private let previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"), - client: Client(server: ""), - routerPath: RouterPath()) // translate from latin button + private let previewStatusViewModel = StatusRowViewModel( + status: Status.placeholder(forSettings: true, language: "la"), + client: Client(server: ""), + routerPath: RouterPath()) // translate from latin button var body: some View { ZStack(alignment: .top) { @@ -53,30 +54,30 @@ struct DisplaySettingsView: View { .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) #endif - .task(id: localValues.tintColor) { - do { try await Task.sleep(for: .microseconds(500)) } catch {} - theme.tintColor = localValues.tintColor - } - .task(id: localValues.primaryBackgroundColor) { - do { try await Task.sleep(for: .microseconds(500)) } catch {} - theme.primaryBackgroundColor = localValues.primaryBackgroundColor - } - .task(id: localValues.secondaryBackgroundColor) { - do { try await Task.sleep(for: .microseconds(500)) } catch {} - theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor - } - .task(id: localValues.labelColor) { - do { try await Task.sleep(for: .microseconds(500)) } catch {} - theme.labelColor = localValues.labelColor - } - .task(id: localValues.lineSpacing) { - do { try await Task.sleep(for: .microseconds(500)) } catch {} - theme.lineSpacing = localValues.lineSpacing - } - .task(id: localValues.fontSizeScale) { - do { try await Task.sleep(for: .microseconds(500)) } catch {} - theme.fontSizeScale = localValues.fontSizeScale - } + .task(id: localValues.tintColor) { + do { try await Task.sleep(for: .microseconds(500)) } catch {} + theme.tintColor = localValues.tintColor + } + .task(id: localValues.primaryBackgroundColor) { + do { try await Task.sleep(for: .microseconds(500)) } catch {} + theme.primaryBackgroundColor = localValues.primaryBackgroundColor + } + .task(id: localValues.secondaryBackgroundColor) { + do { try await Task.sleep(for: .microseconds(500)) } catch {} + theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor + } + .task(id: localValues.labelColor) { + do { try await Task.sleep(for: .microseconds(500)) } catch {} + theme.labelColor = localValues.labelColor + } + .task(id: localValues.lineSpacing) { + do { try await Task.sleep(for: .microseconds(500)) } catch {} + theme.lineSpacing = localValues.lineSpacing + } + .task(id: localValues.fontSizeScale) { + do { try await Task.sleep(for: .microseconds(500)) } catch {} + theme.fontSizeScale = localValues.fontSizeScale + } #if !os(visionOS) examplePost #endif @@ -96,8 +97,10 @@ struct DisplaySettingsView: View { Rectangle() .fill(theme.secondaryBackgroundColor) .frame(height: 30) - .mask(LinearGradient(gradient: Gradient(colors: [theme.secondaryBackgroundColor, .clear]), - startPoint: .top, endPoint: .bottom)) + .mask( + LinearGradient( + gradient: Gradient(colors: [theme.secondaryBackgroundColor, .clear]), + startPoint: .top, endPoint: .bottom)) } } @@ -109,8 +112,11 @@ struct DisplaySettingsView: View { themeSelectorButton Group { ColorPicker("settings.display.theme.tint", selection: $localValues.tintColor) - ColorPicker("settings.display.theme.background", selection: $localValues.primaryBackgroundColor) - ColorPicker("settings.display.theme.secondary-background", selection: $localValues.secondaryBackgroundColor) + ColorPicker( + "settings.display.theme.background", selection: $localValues.primaryBackgroundColor) + ColorPicker( + "settings.display.theme.secondary-background", + selection: $localValues.secondaryBackgroundColor) ColorPicker("settings.display.theme.text-color", selection: $localValues.labelColor) } .disabled(theme.followSystemColorScheme) @@ -129,35 +135,40 @@ struct DisplaySettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } private var fontSection: some View { Section("settings.display.section.font") { - Picker("settings.display.font", selection: .init(get: { () -> FontState in - if theme.chosenFont?.fontName == "OpenDyslexic-Regular" { - return FontState.openDyslexic - } else if theme.chosenFont?.fontName == "AtkinsonHyperlegible-Regular" { - return FontState.hyperLegible - } else if theme.chosenFont?.fontName == ".AppleSystemUIFontRounded-Regular" { - return FontState.SFRounded - } - return theme.chosenFontData != nil ? FontState.custom : FontState.system - }, set: { newValue in - switch newValue { - case .system: - theme.chosenFont = nil - case .openDyslexic: - theme.chosenFont = UIFont(name: "OpenDyslexic", size: 1) - case .hyperLegible: - theme.chosenFont = UIFont(name: "Atkinson Hyperlegible", size: 1) - case .SFRounded: - theme.chosenFont = UIFont.systemFont(ofSize: 1).rounded() - case .custom: - isFontSelectorPresented = true - } - })) { + Picker( + "settings.display.font", + selection: .init( + get: { () -> FontState in + if theme.chosenFont?.fontName == "OpenDyslexic-Regular" { + return FontState.openDyslexic + } else if theme.chosenFont?.fontName == "AtkinsonHyperlegible-Regular" { + return FontState.hyperLegible + } else if theme.chosenFont?.fontName == ".AppleSystemUIFontRounded-Regular" { + return FontState.SFRounded + } + return theme.chosenFontData != nil ? FontState.custom : FontState.system + }, + set: { newValue in + switch newValue { + case .system: + theme.chosenFont = nil + case .openDyslexic: + theme.chosenFont = UIFont(name: "OpenDyslexic", size: 1) + case .hyperLegible: + theme.chosenFont = UIFont(name: "Atkinson Hyperlegible", size: 1) + case .SFRounded: + theme.chosenFont = UIFont.systemFont(ofSize: 1).rounded() + case .custom: + isFontSelectorPresented = true + } + }) + ) { ForEach(FontState.allCases, id: \.rawValue) { fontState in Text(fontState.title).tag(fontState) } @@ -165,7 +176,7 @@ struct DisplaySettingsView: View { .navigationDestination(isPresented: $isFontSelectorPresented, destination: { FontPicker() }) VStack { - Slider(value: $localValues.fontSizeScale, in: 0.5 ... 1.5, step: 0.1) + Slider(value: $localValues.fontSizeScale, in: 0.5...1.5, step: 0.1) Text("settings.display.font.scaling-\(String(format: "%.1f", localValues.fontSizeScale))") .font(.scaledBody) } @@ -174,16 +185,18 @@ struct DisplaySettingsView: View { } VStack { - Slider(value: $localValues.lineSpacing, in: 0.4 ... 10.0, step: 0.2) - Text("settings.display.font.line-spacing-\(String(format: "%.1f", localValues.lineSpacing))") - .font(.scaledBody) + Slider(value: $localValues.lineSpacing, in: 0.4...10.0, step: 0.2) + Text( + "settings.display.font.line-spacing-\(String(format: "%.1f", localValues.lineSpacing))" + ) + .font(.scaledBody) } .alignmentGuide(.listRowSeparatorLeading) { d in d[.leading] } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -224,13 +237,18 @@ struct DisplaySettingsView: View { Toggle("settings.display.show-reply-indentation", isOn: $userPreferences.showReplyIndentation) if userPreferences.showReplyIndentation { VStack { - Slider(value: .init(get: { - Double(userPreferences.maxReplyIndentation) - }, set: { newVal in - userPreferences.maxReplyIndentation = UInt(newVal) - }), in: 1 ... 20, step: 1) - Text("settings.display.max-reply-indentation-\(String(userPreferences.maxReplyIndentation))") - .font(.scaledBody) + Slider( + value: .init( + get: { + Double(userPreferences.maxReplyIndentation) + }, + set: { newVal in + userPreferences.maxReplyIndentation = UInt(newVal) + }), in: 1...20, step: 1) + Text( + "settings.display.max-reply-indentation-\(String(userPreferences.maxReplyIndentation))" + ) + .font(.scaledBody) } .alignmentGuide(.listRowSeparatorLeading) { d in d[.leading] @@ -241,7 +259,7 @@ struct DisplaySettingsView: View { Toggle("Compact Layout", isOn: $theme.compactLayoutPadding) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -254,7 +272,7 @@ struct DisplaySettingsView: View { Toggle("settings.display.show-ipad-column", isOn: $userPreferences.showiPadSecondaryColumn) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } } @@ -268,7 +286,7 @@ struct DisplaySettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } diff --git a/IceCubesApp/App/Tabs/Settings/HapticSettingsView.swift b/IceCubesApp/App/Tabs/Settings/HapticSettingsView.swift index e5e45e97..cd1d3321 100644 --- a/IceCubesApp/App/Tabs/Settings/HapticSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/HapticSettingsView.swift @@ -18,7 +18,7 @@ struct HapticSettingsView: View { Toggle("settings.haptic.buttons", isOn: $userPreferences.hapticButtonPressEnabled) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .navigationTitle("settings.haptic.navigation-title") diff --git a/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift b/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift index d1800ce8..38caf182 100644 --- a/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift +++ b/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift @@ -17,7 +17,8 @@ struct IconSelectorView: View { } case primary = 0 - case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8, alt9, alt10, alt11, alt12, alt13, alt14, alt15 + case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8, alt9, alt10, alt11, alt12, alt13, alt14, + alt15 case alt16, alt17, alt18, alt19, alt20, alt21 case alt22, alt23, alt24, alt25, alt26 case alt27, alt28, alt29 @@ -32,7 +33,7 @@ struct IconSelectorView: View { var appIconName: String { return "AppIconAlternate\(rawValue)" } - + var previewImageName: String { return "AppIconAlternate\(rawValue)-image" } @@ -44,25 +45,45 @@ struct IconSelectorView: View { let icons: [Icon] static let items = [ - IconSelector(title: "settings.app.icon.official".localized, icons: [ - .primary, .alt46, .alt1, .alt2, .alt3, .alt4, - .alt5, .alt6, .alt7, .alt8, - .alt9, .alt10, .alt11, .alt12, .alt13, .alt14, .alt15, - .alt16, .alt17, .alt18, .alt19, .alt20, .alt21]), - IconSelector(title: "\("settings.app.icon.designed-by".localized) Albert Kinng", icons: [.alt22, .alt23, .alt24, .alt25, .alt26]), - IconSelector(title: "\("settings.app.icon.designed-by".localized) Dan van Moll", icons: [.alt27, .alt28, .alt29]), - IconSelector(title: "\("settings.app.icon.designed-by".localized) Chanhwi Joo (GitHub @te6-in)", icons: [.alt30, .alt31, .alt32, .alt33, .alt34, .alt35, .alt36]), - IconSelector(title: "\("settings.app.icon.designed-by".localized) W. Kovács Ágnes (@wildgica)", icons: [.alt37]), - IconSelector(title: "\("settings.app.icon.designed-by".localized) Duncan Horne", icons: [.alt38]), - IconSelector(title: "\("settings.app.icon.designed-by".localized) BeAware@social.beaware.live", icons: [.alt39, .alt40, .alt41, .alt42, .alt43]), - IconSelector(title: "\("settings.app.icon.designed-by".localized) Simone Margio", icons: [.alt44, .alt45]), - IconSelector(title: "\("settings.app.icon.designed-by".localized) Peter Broqvist (@PKB)", icons: [.alt47, .alt48]), - IconSelector(title: "\("settings.app.icon.designed-by".localized) Oz Tsori (@oztsori)", icons: [.alt49]), + IconSelector( + title: "settings.app.icon.official".localized, + icons: [ + .primary, .alt46, .alt1, .alt2, .alt3, .alt4, + .alt5, .alt6, .alt7, .alt8, + .alt9, .alt10, .alt11, .alt12, .alt13, .alt14, .alt15, + .alt16, .alt17, .alt18, .alt19, .alt20, .alt21, + ]), + IconSelector( + title: "\("settings.app.icon.designed-by".localized) Albert Kinng", + icons: [.alt22, .alt23, .alt24, .alt25, .alt26]), + IconSelector( + title: "\("settings.app.icon.designed-by".localized) Dan van Moll", + icons: [.alt27, .alt28, .alt29]), + IconSelector( + title: "\("settings.app.icon.designed-by".localized) Chanhwi Joo (GitHub @te6-in)", + icons: [.alt30, .alt31, .alt32, .alt33, .alt34, .alt35, .alt36]), + IconSelector( + title: "\("settings.app.icon.designed-by".localized) W. Kovács Ágnes (@wildgica)", + icons: [.alt37]), + IconSelector( + title: "\("settings.app.icon.designed-by".localized) Duncan Horne", icons: [.alt38]), + IconSelector( + title: "\("settings.app.icon.designed-by".localized) BeAware@social.beaware.live", + icons: [.alt39, .alt40, .alt41, .alt42, .alt43]), + IconSelector( + title: "\("settings.app.icon.designed-by".localized) Simone Margio", + icons: [.alt44, .alt45]), + IconSelector( + title: "\("settings.app.icon.designed-by".localized) Peter Broqvist (@PKB)", + icons: [.alt47, .alt48]), + IconSelector( + title: "\("settings.app.icon.designed-by".localized) Oz Tsori (@oztsori)", icons: [.alt49]), ] } @Environment(Theme.self) private var theme - @State private var currentIcon = UIApplication.shared.alternateIconName ?? Icon.primary.appIconName + @State private var currentIcon = + UIApplication.shared.alternateIconName ?? Icon.primary.appIconName private let columns = [GridItem(.adaptive(minimum: 125, maximum: 1024))] @@ -82,7 +103,7 @@ struct IconSelectorView: View { .navigationTitle("settings.app.icon.navigation-title") } #if !os(visionOS) - .background(theme.primaryBackgroundColor) + .background(theme.primaryBackgroundColor) #endif } diff --git a/IceCubesApp/App/Tabs/Settings/InstanceInfoView.swift b/IceCubesApp/App/Tabs/Settings/InstanceInfoView.swift index 987174cc..eca5d5ec 100644 --- a/IceCubesApp/App/Tabs/Settings/InstanceInfoView.swift +++ b/IceCubesApp/App/Tabs/Settings/InstanceInfoView.swift @@ -38,7 +38,7 @@ public struct InstanceInfoSection: View { LabeledContent("instance.info.domains", value: format(instance.stats.domainCount)) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif if let rules = instance.rules { @@ -48,7 +48,7 @@ public struct InstanceInfoSection: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } } diff --git a/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift b/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift index 57b5b65f..7f5846fc 100644 --- a/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift +++ b/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift @@ -18,78 +18,106 @@ struct PushNotificationsView: View { var body: some View { Form { Section { - Toggle(isOn: .init(get: { - subscription.isEnabled - }, set: { newValue in - subscription.isEnabled = newValue - if newValue { - updateSubscription() - } else { - deleteSubscription() - } - })) { + Toggle( + isOn: .init( + get: { + subscription.isEnabled + }, + set: { newValue in + subscription.isEnabled = newValue + if newValue { + updateSubscription() + } else { + deleteSubscription() + } + }) + ) { Text("settings.push.main-toggle") } } footer: { Text("settings.push.main-toggle.description") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif if subscription.isEnabled { Section { - Toggle(isOn: .init(get: { - subscription.isMentionNotificationEnabled - }, set: { newValue in - subscription.isMentionNotificationEnabled = newValue - updateSubscription() - })) { + Toggle( + isOn: .init( + get: { + subscription.isMentionNotificationEnabled + }, + set: { newValue in + subscription.isMentionNotificationEnabled = newValue + updateSubscription() + }) + ) { Label("settings.push.mentions", systemImage: "at") } - Toggle(isOn: .init(get: { - subscription.isFollowNotificationEnabled - }, set: { newValue in - subscription.isFollowNotificationEnabled = newValue - updateSubscription() - })) { + Toggle( + isOn: .init( + get: { + subscription.isFollowNotificationEnabled + }, + set: { newValue in + subscription.isFollowNotificationEnabled = newValue + updateSubscription() + }) + ) { Label("settings.push.follows", systemImage: "person.badge.plus") } - Toggle(isOn: .init(get: { - subscription.isFavoriteNotificationEnabled - }, set: { newValue in - subscription.isFavoriteNotificationEnabled = newValue - updateSubscription() - })) { + Toggle( + isOn: .init( + get: { + subscription.isFavoriteNotificationEnabled + }, + set: { newValue in + subscription.isFavoriteNotificationEnabled = newValue + updateSubscription() + }) + ) { Label("settings.push.favorites", systemImage: "star") } - Toggle(isOn: .init(get: { - subscription.isReblogNotificationEnabled - }, set: { newValue in - subscription.isReblogNotificationEnabled = newValue - updateSubscription() - })) { + Toggle( + isOn: .init( + get: { + subscription.isReblogNotificationEnabled + }, + set: { newValue in + subscription.isReblogNotificationEnabled = newValue + updateSubscription() + }) + ) { Label("settings.push.boosts", image: "Rocket") } - Toggle(isOn: .init(get: { - subscription.isPollNotificationEnabled - }, set: { newValue in - subscription.isPollNotificationEnabled = newValue - updateSubscription() - })) { + Toggle( + isOn: .init( + get: { + subscription.isPollNotificationEnabled + }, + set: { newValue in + subscription.isPollNotificationEnabled = newValue + updateSubscription() + }) + ) { Label("settings.push.polls", systemImage: "chart.bar") } - Toggle(isOn: .init(get: { - subscription.isNewPostsNotificationEnabled - }, set: { newValue in - subscription.isNewPostsNotificationEnabled = newValue - updateSubscription() - })) { + Toggle( + isOn: .init( + get: { + subscription.isNewPostsNotificationEnabled + }, + set: { newValue in + subscription.isNewPostsNotificationEnabled = newValue + updateSubscription() + }) + ) { Label("settings.push.new-posts", systemImage: "bubble.right") } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -106,7 +134,7 @@ struct PushNotificationsView: View { Text("settings.push.duplicate.footer") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .navigationTitle("settings.push.navigation-title") @@ -114,9 +142,9 @@ struct PushNotificationsView: View { .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) #endif - .task { - await subscription.fetchSubscription() - } + .task { + await subscription.fetchSubscription() + } } private func updateSubscription() { diff --git a/IceCubesApp/App/Tabs/Settings/RecenTagsSettingView.swift b/IceCubesApp/App/Tabs/Settings/RecenTagsSettingView.swift index 0f8ac598..a62094b5 100644 --- a/IceCubesApp/App/Tabs/Settings/RecenTagsSettingView.swift +++ b/IceCubesApp/App/Tabs/Settings/RecenTagsSettingView.swift @@ -29,7 +29,7 @@ struct RecenTagsSettingView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .navigationTitle("settings.general.recent-tags") @@ -37,8 +37,8 @@ struct RecenTagsSettingView: View { #if !os(visionOS) .background(theme.secondaryBackgroundColor) #endif - .toolbar { - EditButton() - } + .toolbar { + EditButton() + } } } diff --git a/IceCubesApp/App/Tabs/Settings/RemoteTimelinesSettingView.swift b/IceCubesApp/App/Tabs/Settings/RemoteTimelinesSettingView.swift index 94a35e49..10c17792 100644 --- a/IceCubesApp/App/Tabs/Settings/RemoteTimelinesSettingView.swift +++ b/IceCubesApp/App/Tabs/Settings/RemoteTimelinesSettingView.swift @@ -22,7 +22,7 @@ struct RemoteTimelinesSettingView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif Button { routerPath.presentedSheet = .addRemoteLocalTimeline @@ -30,7 +30,7 @@ struct RemoteTimelinesSettingView: View { Label("settings.timeline.add", systemImage: "badge.plus.radiowaves.right") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .navigationTitle("settings.general.remote-timelines") @@ -38,8 +38,8 @@ struct RemoteTimelinesSettingView: View { #if !os(visionOS) .background(theme.secondaryBackgroundColor) #endif - .toolbar { - EditButton() - } + .toolbar { + EditButton() + } } } diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index f8ad493a..dde8e506 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -47,51 +47,53 @@ struct SettingsTabs: View { #if !os(visionOS) .background(theme.secondaryBackgroundColor) #endif - .navigationTitle(Text("settings.title")) - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar) - .toolbar { - if isModal { - ToolbarItem { - Button { - dismiss() - } label: { - Text("action.done").bold() - } + .navigationTitle(Text("settings.title")) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar) + .toolbar { + if isModal { + ToolbarItem { + Button { + dismiss() + } label: { + Text("action.done").bold() } } - if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal { - SecondaryColumnToolbarItem() - } } - .withAppRouter() - .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) - .onAppear { - startingPoint = RouterPath.settingsStartingPoint - RouterPath.settingsStartingPoint = nil + if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, + !isModal + { + SecondaryColumnToolbarItem() } - .navigationDestination(item: $startingPoint) { targetView in - switch targetView { - case .display: - DisplaySettingsView() - case .haptic: - HapticSettingsView() - case .remoteTimelines: - RemoteTimelinesSettingView() - case .tagGroups: - TagsGroupSettingView() - case .recentTags: - RecenTagsSettingView() - case .content: - ContentSettingsView() - case .swipeActions: - SwipeActionsSettingsView() - case .tabAndSidebarEntries: - EmptyView() - case .translation: - TranslationSettingsView() - } + } + .withAppRouter() + .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) + .onAppear { + startingPoint = RouterPath.settingsStartingPoint + RouterPath.settingsStartingPoint = nil + } + .navigationDestination(item: $startingPoint) { targetView in + switch targetView { + case .display: + DisplaySettingsView() + case .haptic: + HapticSettingsView() + case .remoteTimelines: + RemoteTimelinesSettingView() + case .tagGroups: + TagsGroupSettingView() + case .recentTags: + RecenTagsSettingView() + case .content: + ContentSettingsView() + case .swipeActions: + SwipeActionsSettingsView() + case .tabAndSidebarEntries: + EmptyView() + case .translation: + TranslationSettingsView() } + } } .onAppear { routerPath.client = client @@ -137,13 +139,13 @@ struct SettingsTabs: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } private func logoutAccount(account: AppAccount) async { if let token = account.oauthToken, - let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) + let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) { let client = Client(server: account.server, oauthToken: token) await timelineCache.clearCache(for: client.id) @@ -188,7 +190,9 @@ struct SettingsTabs: View { NavigationLink(destination: TabbarEntriesSettingsView()) { Label("settings.general.tabbarEntries", systemImage: "platter.filled.bottom.iphone") } - } else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + } else if UIDevice.current.userInterfaceIdiom == .pad + || UIDevice.current.userInterfaceIdiom == .mac + { NavigationLink(destination: SidebarEntriesSettingsView()) { Label("settings.general.sidebarEntries", systemImage: "sidebar.squares.leading") } @@ -204,7 +208,7 @@ struct SettingsTabs: View { #endif } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -245,10 +249,10 @@ struct SettingsTabs: View { Text("settings.section.other.footer") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } - + @ViewBuilder private var postStreamingSection: some View { @Bindable var preferences = preferences @@ -259,13 +263,15 @@ struct SettingsTabs: View { } header: { Text("Streaming") } footer: { - Text("Enabling post streaming will automatically add new posts at the top of your home timeline. Disable if you get performance issues.") + Text( + "Enabling post streaming will automatically add new posts at the top of your home timeline. Disable if you get performance issues." + ) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } - + @ViewBuilder private var AISection: some View { @Bindable var preferences = preferences @@ -276,10 +282,12 @@ struct SettingsTabs: View { } header: { Text("AI") } footer: { - Text("Disable to hide AI assisted tool options such as copywritting and alt-image description generated using AI. Uses OpenAI API. See our Privacy Policy for more information.") + Text( + "Disable to hide AI assisted tool options such as copywritting and alt-image description generated using AI. Uses OpenAI API. See our Privacy Policy for more information." + ) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -290,7 +298,8 @@ struct SettingsTabs: View { Label { Text("settings.app.icon") } icon: { - let icon = IconSelectorView.Icon(string: UIApplication.shared.alternateIconName ?? "AppIcon") + let icon = IconSelectorView.Icon( + string: UIApplication.shared.alternateIconName ?? "AppIcon") if let image: UIImage = .init(named: icon.previewImageName) { Image(uiImage: image) .resizable() @@ -313,7 +322,9 @@ struct SettingsTabs: View { Label("settings.app.support", systemImage: "wand.and.stars") } - if let reviewURL = URL(string: "https://apps.apple.com/app/id\(AppInfo.appStoreAppId)?action=write-review") { + if let reviewURL = URL( + string: "https://apps.apple.com/app/id\(AppInfo.appStoreAppId)?action=write-review") + { Link(destination: reviewURL) { Label("settings.rate", systemImage: "link") } @@ -326,7 +337,7 @@ struct SettingsTabs: View { } label: { Label("settings.app.about", systemImage: "info.circle") } - + NavigationLink { WishlistView() } label: { @@ -337,11 +348,12 @@ struct SettingsTabs: View { Text("settings.section.app") } footer: { if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { - Text("settings.section.app.footer \(appVersion)").frame(maxWidth: .infinity, alignment: .center) + Text("settings.section.app.footer \(appVersion)").frame( + maxWidth: .infinity, alignment: .center) } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -391,7 +403,7 @@ struct SettingsTabs: View { Text("Remove all cached images and videos") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } } diff --git a/IceCubesApp/App/Tabs/Settings/SidebarEntriesSettingsView.swift b/IceCubesApp/App/Tabs/Settings/SidebarEntriesSettingsView.swift index 0d1a157f..fe83f35a 100644 --- a/IceCubesApp/App/Tabs/Settings/SidebarEntriesSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/SidebarEntriesSettingsView.swift @@ -23,7 +23,7 @@ struct SidebarEntriesSettingsView: View { .onMove(perform: move) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .environment(\.editMode, .constant(.active)) diff --git a/IceCubesApp/App/Tabs/Settings/SupportAppView.swift b/IceCubesApp/App/Tabs/Settings/SupportAppView.swift index bbe1b5f4..d62408b3 100644 --- a/IceCubesApp/App/Tabs/Settings/SupportAppView.swift +++ b/IceCubesApp/App/Tabs/Settings/SupportAppView.swift @@ -72,21 +72,37 @@ struct SupportAppView: View { .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) #endif - .alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: { - Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") } - }, message: { + .alert( + "settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, + actions: { + Button { + purchaseSuccessDisplayed = false + } label: { + Text("alert.button.ok") + } + }, + message: { Text("settings.support.alert.message") - }) - .alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: { - Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") } - }, message: { - Text("settings.support.alert.error.message") - }) - .onAppear { - loadingProducts = true - fetchStoreProducts() - refreshUserInfo() } + ) + .alert( + "alert.error", isPresented: $purchaseErrorDisplayed, + actions: { + Button { + purchaseErrorDisplayed = false + } label: { + Text("alert.button.ok") + } + }, + message: { + Text("settings.support.alert.error.message") + } + ) + .onAppear { + loadingProducts = true + fetchStoreProducts() + refreshUserInfo() + } } private func purchase(product: StoreProduct) async { @@ -107,7 +123,8 @@ struct SupportAppView: View { private func fetchStoreProducts() { Purchases.shared.getProducts(Tip.allCases.map(\.productId)) { products in subscription = products.first(where: { $0.productIdentifier == Tip.supporter.productId }) - self.products = products.filter { $0.productIdentifier != Tip.supporter.productId }.sorted(by: { $0.price < $1.price }) + self.products = products.filter { $0.productIdentifier != Tip.supporter.productId }.sorted( + by: { $0.price < $1.price }) withAnimation { loadingProducts = false } @@ -153,7 +170,7 @@ struct SupportAppView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -166,15 +183,15 @@ struct SupportAppView: View { if customerInfo?.entitlements["Supporter"]?.isActive == true { Text(Image(systemName: "checkmark.seal.fill")) .foregroundColor(theme.tintColor) - .baselineOffset(-1) + - Text("settings.support.supporter.subscribed") + .baselineOffset(-1) + + Text("settings.support.supporter.subscribed") .font(.scaledSubheadline) } else { VStack(alignment: .leading) { Text(Image(systemName: "checkmark.seal.fill")) .foregroundColor(theme.tintColor) - .baselineOffset(-1) + - Text(Tip.supporter.title) + .baselineOffset(-1) + + Text(Tip.supporter.title) .font(.scaledSubheadline) Text(Tip.supporter.subtitle) .font(.scaledFootnote) @@ -192,7 +209,7 @@ struct SupportAppView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -219,7 +236,7 @@ struct SupportAppView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -240,7 +257,7 @@ struct SupportAppView: View { Text("settings.support.restore-purchase.explanation") } #if !os(visionOS) - .listRowBackground(theme.secondaryBackgroundColor) + .listRowBackground(theme.secondaryBackgroundColor) #endif } @@ -262,7 +279,7 @@ struct SupportAppView: View { } } #if !os(visionOS) - .listRowBackground(theme.secondaryBackgroundColor) + .listRowBackground(theme.secondaryBackgroundColor) #endif } diff --git a/IceCubesApp/App/Tabs/Settings/SwipeActionsSettingsView.swift b/IceCubesApp/App/Tabs/Settings/SwipeActionsSettingsView.swift index 47443cc9..f63d1fa1 100644 --- a/IceCubesApp/App/Tabs/Settings/SwipeActionsSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/SwipeActionsSettingsView.swift @@ -14,32 +14,40 @@ struct SwipeActionsSettingsView: View { Label("settings.swipeactions.status.leading", systemImage: "arrow.right") .foregroundColor(.secondary) - createStatusActionPicker(selection: $userPreferences.swipeActionsStatusLeadingLeft, - label: "settings.swipeactions.primary") - .onChange(of: userPreferences.swipeActionsStatusLeadingLeft) { _, action in - if action == .none { - userPreferences.swipeActionsStatusLeadingRight = .none - } + createStatusActionPicker( + selection: $userPreferences.swipeActionsStatusLeadingLeft, + label: "settings.swipeactions.primary" + ) + .onChange(of: userPreferences.swipeActionsStatusLeadingLeft) { _, action in + if action == .none { + userPreferences.swipeActionsStatusLeadingRight = .none } + } - createStatusActionPicker(selection: $userPreferences.swipeActionsStatusLeadingRight, - label: "settings.swipeactions.secondary") - .disabled(userPreferences.swipeActionsStatusLeadingLeft == .none) + createStatusActionPicker( + selection: $userPreferences.swipeActionsStatusLeadingRight, + label: "settings.swipeactions.secondary" + ) + .disabled(userPreferences.swipeActionsStatusLeadingLeft == .none) Label("settings.swipeactions.status.trailing", systemImage: "arrow.left") .foregroundColor(.secondary) - createStatusActionPicker(selection: $userPreferences.swipeActionsStatusTrailingRight, - label: "settings.swipeactions.primary") - .onChange(of: userPreferences.swipeActionsStatusTrailingRight) { _, action in - if action == .none { - userPreferences.swipeActionsStatusTrailingLeft = .none - } + createStatusActionPicker( + selection: $userPreferences.swipeActionsStatusTrailingRight, + label: "settings.swipeactions.primary" + ) + .onChange(of: userPreferences.swipeActionsStatusTrailingRight) { _, action in + if action == .none { + userPreferences.swipeActionsStatusTrailingLeft = .none } + } - createStatusActionPicker(selection: $userPreferences.swipeActionsStatusTrailingLeft, - label: "settings.swipeactions.secondary") - .disabled(userPreferences.swipeActionsStatusTrailingRight == .none) + createStatusActionPicker( + selection: $userPreferences.swipeActionsStatusTrailingLeft, + label: "settings.swipeactions.secondary" + ) + .disabled(userPreferences.swipeActionsStatusTrailingRight == .none) } header: { Text("settings.swipeactions.status") @@ -47,11 +55,14 @@ struct SwipeActionsSettingsView: View { Text("settings.swipeactions.status.explanation") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif Section { - Picker(selection: $userPreferences.swipeActionsIconStyle, label: Text("settings.swipeactions.icon-style")) { + Picker( + selection: $userPreferences.swipeActionsIconStyle, + label: Text("settings.swipeactions.icon-style") + ) { ForEach(UserPreferences.SwipeActionsIconStyle.allCases, id: \.rawValue) { style in Text(style.description).tag(style) } @@ -65,7 +76,7 @@ struct SwipeActionsSettingsView: View { Text("settings.swipeactions.use-theme-colors-explanation") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .navigationTitle("settings.swipeactions.navigation-title") @@ -75,7 +86,9 @@ struct SwipeActionsSettingsView: View { #endif } - private func createStatusActionPicker(selection: Binding, label: LocalizedStringKey) -> some View { + private func createStatusActionPicker(selection: Binding, label: LocalizedStringKey) + -> some View + { Picker(selection: selection, label: Text(label)) { Section { Text(StatusAction.none.displayName()).tag(StatusAction.none) diff --git a/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift index 13099f19..353927eb 100644 --- a/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift @@ -50,14 +50,14 @@ struct TabbarEntriesSettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif Section { Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .navigationTitle("settings.general.tabbarEntries") diff --git a/IceCubesApp/App/Tabs/Settings/TagsGroupSettingView.swift b/IceCubesApp/App/Tabs/Settings/TagsGroupSettingView.swift index ba0b3995..1b6e08fc 100644 --- a/IceCubesApp/App/Tabs/Settings/TagsGroupSettingView.swift +++ b/IceCubesApp/App/Tabs/Settings/TagsGroupSettingView.swift @@ -26,7 +26,7 @@ struct TagsGroupSettingView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif Button { @@ -35,7 +35,7 @@ struct TagsGroupSettingView: View { Label("timeline.filter.add-tag-groups", systemImage: "plus") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .navigationTitle("timeline.filter.tag-groups") @@ -43,8 +43,8 @@ struct TagsGroupSettingView: View { #if !os(visionOS) .background(theme.secondaryBackgroundColor) #endif - .toolbar { - EditButton() - } + .toolbar { + EditButton() + } } } diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index 6337226d..55a8a36c 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -19,7 +19,7 @@ struct TranslationSettingsView: View { .textContentType(.password) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif if apiKey.isEmpty { @@ -30,7 +30,7 @@ struct TranslationSettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } } @@ -42,11 +42,11 @@ struct TranslationSettingsView: View { .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) #endif - .onChange(of: apiKey) { - writeNewValue() - } - .onAppear(perform: updatePrefs) - .onAppear(perform: readValue) + .onChange(of: apiKey) { + writeNewValue() + } + .onAppear(perform: updatePrefs) + .onAppear(perform: readValue) } @ViewBuilder @@ -58,7 +58,7 @@ struct TranslationSettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -99,19 +99,20 @@ struct TranslationSettingsView: View { Text("settings.translation.auto-detect-post-language-footer") } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @ViewBuilder private var backgroundAPIKey: some View { if preferences.preferredTranslationType != .useDeepl, - !apiKey.isEmpty + !apiKey.isEmpty { Section { Text("The DeepL API Key is still stored!") if preferences.preferredTranslationType == .useServerIfPossible { - Text("It can however still be used as a fallback for your instance's translation service.") + Text( + "It can however still be used as a fallback for your instance's translation service.") } Button(role: .destructive) { withAnimation { @@ -123,7 +124,7 @@ struct TranslationSettingsView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } } diff --git a/IceCubesApp/App/Tabs/Settings/WishlistView.swift b/IceCubesApp/App/Tabs/Settings/WishlistView.swift index 164f7db2..eea4481d 100644 --- a/IceCubesApp/App/Tabs/Settings/WishlistView.swift +++ b/IceCubesApp/App/Tabs/Settings/WishlistView.swift @@ -3,6 +3,6 @@ import WishKit struct WishlistView: View { var body: some View { - WishKit.view + WishKit.FeedbackListView() } } diff --git a/IceCubesApp/App/Tabs/TagGroup/EditTagGroupView.swift b/IceCubesApp/App/Tabs/TagGroup/EditTagGroupView.swift index 5b8b7572..094c9c37 100644 --- a/IceCubesApp/App/Tabs/TagGroup/EditTagGroupView.swift +++ b/IceCubesApp/App/Tabs/TagGroup/EditTagGroupView.swift @@ -39,7 +39,7 @@ struct EditTagGroupView: View { ) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif Section("add-tag-groups.edit.tags") { @@ -50,7 +50,7 @@ struct EditTagGroupView: View { ) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .formStyle(.grouped) @@ -65,16 +65,16 @@ struct EditTagGroupView: View { .background(theme.secondaryBackgroundColor) .scrollDismissesKeyboard(.interactively) #endif - .toolbar { - CancelToolbarItem() - ToolbarItem(placement: .navigationBarTrailing) { - Button("action.save", action: { save() }) - .disabled(!tagGroup.isValid) - } - } - .onAppear { - focusedField = .title + .toolbar { + CancelToolbarItem() + ToolbarItem(placement: .navigationBarTrailing) { + Button("action.save", action: { save() }) + .disabled(!tagGroup.isValid) } + } + .onAppear { + focusedField = .title + } } } @@ -140,8 +140,7 @@ private struct TitleInputView: View { var warningText: LocalizedStringKey { if case let .invalid(description) = titleValidationStatus { return description - } else if - isNewGroup, + } else if isNewGroup, tagGroups.contains(where: { $0.title == title }) { return "\(title) add-tag-groups.edit.title.field.warning.already-exists" @@ -180,7 +179,7 @@ private struct SymbolInputView: View { } if case let .invalid(description) = selectedSymbolValidationStatus, - focusedField == .symbol + focusedField == .symbol { Text(description).warningLabel() } @@ -210,7 +209,9 @@ private struct TagsInputView: View { HStack { Text(tag) Spacer() - Button { deleteTag(tag) } label: { + Button { + deleteTag(tag) + } label: { Image(systemName: "trash") .foregroundStyle(.red) } @@ -240,7 +241,9 @@ private struct TagsInputView: View { Spacer() if !newTag.isEmpty, !tags.contains(newTag) { - Button { addNewTag() } label: { + Button { + addNewTag() + } label: { Image(systemName: "checkmark.circle.fill").tint(.green) } } @@ -281,7 +284,7 @@ private struct TagsInputView: View { private func addTag(_ tag: String) { guard !tag.isEmpty, - !tags.contains(tag) + !tags.contains(tag) else { return } tags.append(tag) @@ -347,12 +350,15 @@ private struct SymbolSearchResultsView: View { var validationStatus: ValidationStatus { if results.isEmpty { if symbolQuery == selectedSymbol, - !symbolQuery.isEmpty, - results.count == 0 + !symbolQuery.isEmpty, + results.count == 0 { - .invalid(description: "\(symbolQuery) add-tag-groups.edit.tags.field.warning.search-results.already-selected") + .invalid( + description: + "\(symbolQuery) add-tag-groups.edit.tags.field.warning.search-results.already-selected") } else { - .invalid(description: "add-tag-groups.edit.tags.field.warning.search-results.no-symbol-found") + .invalid( + description: "add-tag-groups.edit.tags.field.warning.search-results.no-symbol-found") } } else { .valid @@ -385,7 +391,8 @@ extension TagGroup { if symbolName.isEmpty { return .invalid(description: "add-tag-groups.edit.title.field.warning.no-symbol-selected") } else if !Self.allSymbols.contains(symbolName) { - return .invalid(description: "\(symbolName) add-tag-groups.edit.title.field.warning.invalid-sfsymbol-name") + return .invalid( + description: "\(symbolName) add-tag-groups.edit.title.field.warning.invalid-sfsymbol-name") } return .valid @@ -430,8 +437,7 @@ extension TagGroup { guard !query.isEmpty else { return [] } return allSymbols.filter { - $0.contains(query) && - $0 != excludedSymbol + $0.contains(query) && $0 != excludedSymbol } } diff --git a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift index f34d40d5..de7e4cc3 100644 --- a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift +++ b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift @@ -62,26 +62,28 @@ struct AddRemoteTimelineView: View { .background(theme.secondaryBackgroundColor) .scrollDismissesKeyboard(.immediately) #endif - .toolbar { - CancelToolbarItem() + .toolbar { + CancelToolbarItem() + } + .onChange(of: instanceName) { _, newValue in + instanceNamePublisher.send(newValue) + } + .onReceive( + instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) + ) { newValue in + Task { + let client = Client(server: newValue) + instance = try? await client.get(endpoint: Instances.instance) } - .onChange(of: instanceName) { _, newValue in - instanceNamePublisher.send(newValue) - } - .onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in - Task { - let client = Client(server: newValue) - instance = try? await client.get(endpoint: Instances.instance) - } - } - .onAppear { - isInstanceURLFieldFocused = true - let client = InstanceSocialClient() - let instanceName = instanceName - Task { - instances = await client.fetchInstances(keyword: instanceName) - } + } + .onAppear { + isInstanceURLFieldFocused = true + let client = InstanceSocialClient() + let instanceName = instanceName + Task { + instances = await client.fetchInstances(keyword: instanceName) } + } } } @@ -91,7 +93,10 @@ struct AddRemoteTimelineView: View { ProgressView() .listRowBackground(theme.primaryBackgroundColor) } else { - ForEach(instanceName.isEmpty ? instances : instances.filter { $0.name.contains(instanceName.lowercased()) }) { instance in + ForEach( + instanceName.isEmpty + ? instances : instances.filter { $0.name.contains(instanceName.lowercased()) } + ) { instance in Button { instanceName = instance.name } label: { diff --git a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift index 51e04944..7361e78b 100644 --- a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift @@ -38,17 +38,19 @@ struct TimelineTab: View { var body: some View { NavigationStack(path: $routerPath.path) { - TimelineView(timeline: $timeline, - pinnedFilters: $pinnedFilters, - selectedTagGroup: $selectedTagGroup, - canFilterTimeline: canFilterTimeline) - .withAppRouter() - .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) - .toolbar { - toolbarView - } - .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar) - .id(client.id) + TimelineView( + timeline: $timeline, + pinnedFilters: $pinnedFilters, + selectedTagGroup: $selectedTagGroup, + canFilterTimeline: canFilterTimeline + ) + .withAppRouter() + .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) + .toolbar { + toolbarView + } + .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar) + .id(client.id) } .onAppear { routerPath.client = client @@ -182,7 +184,8 @@ struct TimelineTab: View { Button { timeline = .latest } label: { - Label(TimelineFilter.latest.localizedTitle(), systemImage: TimelineFilter.latest.iconName()) + Label( + TimelineFilter.latest.localizedTitle(), systemImage: TimelineFilter.latest.iconName()) } } if timeline == .home { @@ -190,8 +193,9 @@ struct TimelineTab: View { timeline = .resume } label: { VStack { - Label(TimelineFilter.resume.localizedTitle(), - systemImage: TimelineFilter.resume.iconName()) + Label( + TimelineFilter.resume.localizedTitle(), + systemImage: TimelineFilter.resume.iconName()) } } } @@ -206,10 +210,10 @@ struct TimelineTab: View { withAnimation { if let index { let timeline = pinnedFilters.remove(at: index) - Telemetry.signal("timeline.pin.removed", parameters: ["timeline" : timeline.rawValue]) + Telemetry.signal("timeline.pin.removed", parameters: ["timeline": timeline.rawValue]) } else { pinnedFilters.append(timeline) - Telemetry.signal("timeline.pin.added", parameters: ["timeline" : timeline.rawValue]) + Telemetry.signal("timeline.pin.added", parameters: ["timeline": timeline.rawValue]) } } } label: { @@ -305,11 +309,13 @@ struct TimelineTab: View { } private var contentFilterButton: some View { - Button(action: { - routerPath.presentedSheet = .timelineContentFilter - }, label: { - Label("timeline.content-filter.title", systemSymbol: .line3HorizontalDecrease) - }) + Button( + action: { + routerPath.presentedSheet = .timelineContentFilter + }, + label: { + Label("timeline.content-filter.title", systemSymbol: .line3HorizontalDecrease) + }) } private func resetTimelineFilter() { diff --git a/IceCubesApp/App/Tabs/ToolbarTab.swift b/IceCubesApp/App/Tabs/ToolbarTab.swift index 2e8087b2..a2b4551e 100644 --- a/IceCubesApp/App/Tabs/ToolbarTab.swift +++ b/IceCubesApp/App/Tabs/ToolbarTab.swift @@ -17,7 +17,9 @@ struct ToolbarTab: ToolbarContent { if !isSecondaryColumn { if horizontalSizeClass == .regular { ToolbarItem(placement: .topBarLeading) { - if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + if UIDevice.current.userInterfaceIdiom == .pad + || UIDevice.current.userInterfaceIdiom == .mac + { Button { withAnimation { userPreferences.isSidebarExpanded.toggle() @@ -32,13 +34,16 @@ struct ToolbarTab: ToolbarContent { } } } - statusEditorToolbarItem(routerPath: routerPath, - visibility: userPreferences.postVisibility) - if UIDevice.current.userInterfaceIdiom != .pad || - (UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact) + statusEditorToolbarItem( + routerPath: routerPath, + visibility: userPreferences.postVisibility) + if UIDevice.current.userInterfaceIdiom != .pad + || (UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact) { ToolbarItem(placement: .navigationBarLeading) { - AppAccountsSelectorView(routerPath: routerPath, avatarConfig: theme.avatarShape == .circle ? .badge : .badgeRounded) + AppAccountsSelectorView( + routerPath: routerPath, + avatarConfig: theme.avatarShape == .circle ? .badge : .badgeRounded) } } } diff --git a/IceCubesAppIntents/AppIntentService.swift b/IceCubesAppIntents/AppIntentService.swift index 81d365a0..af79069d 100644 --- a/IceCubesAppIntents/AppIntentService.swift +++ b/IceCubesAppIntents/AppIntentService.swift @@ -4,7 +4,9 @@ import SwiftUI @Observable public class AppIntentService: @unchecked Sendable { struct HandledIntent: Equatable { - static func == (lhs: AppIntentService.HandledIntent, rhs: AppIntentService.HandledIntent) -> Bool { + static func == (lhs: AppIntentService.HandledIntent, rhs: AppIntentService.HandledIntent) + -> Bool + { lhs.id == rhs.id } diff --git a/IceCubesAppIntents/AppShortcuts.swift b/IceCubesAppIntents/AppShortcuts.swift index e36fd43c..f8038c5c 100644 --- a/IceCubesAppIntents/AppShortcuts.swift +++ b/IceCubesAppIntents/AppShortcuts.swift @@ -23,7 +23,7 @@ struct AppShortcuts: AppShortcutsProvider { AppShortcut( intent: TabIntent(), phrases: [ - "Open \(.applicationName)", + "Open \(.applicationName)" ], shortTitle: "Open Ice Cubes", systemImageName: "cube" diff --git a/IceCubesAppIntents/InlinePostIntent.swift b/IceCubesAppIntents/InlinePostIntent.swift index 93cb19ca..15202079 100644 --- a/IceCubesAppIntents/InlinePostIntent.swift +++ b/IceCubesAppIntents/InlinePostIntent.swift @@ -9,10 +9,12 @@ enum PostVisibility: String, AppEnum { case direct, priv, unlisted, pub public static var caseDisplayRepresentations: [PostVisibility: DisplayRepresentation] { - [.direct: "Private", - .priv: "Followers Only", - .unlisted: "Quiet Public", - .pub: "Public"] + [ + .direct: "Private", + .priv: "Followers Only", + .unlisted: "Quiet Public", + .pub: "Public", + ] } static var typeDisplayName: LocalizedStringResource { "Visibility" } @@ -44,12 +46,15 @@ struct InlinePostIntent: AppIntent { @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")) + @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) + let client = Client( + server: account.account.server, version: .v1, oauthToken: account.account.oauthToken) let status = StatusData(status: content, visibility: visibility.toAppVisibility) do { let status: Status = try await client.post(endpoint: Statuses.postStatus(json: status)) diff --git a/IceCubesAppIntents/PostImageIntent.swift b/IceCubesAppIntents/PostImageIntent.swift index 8b312a7b..d4cb1b2d 100644 --- a/IceCubesAppIntents/PostImageIntent.swift +++ b/IceCubesAppIntents/PostImageIntent.swift @@ -3,13 +3,15 @@ import Foundation struct PostImageIntent: AppIntent { static let title: LocalizedStringResource = "Post an image to Mastodon" - static let description: IntentDescription = "Use Ice Cubes to compose a post with an image to Mastodon" + static let description: IntentDescription = + "Use Ice Cubes to compose a post with an image to Mastodon" static let openAppWhenRun: Bool = true - @Parameter(title: "Image", - description: "Image to post on Mastodon", - supportedTypeIdentifiers: ["public.image"], - inputConnectionBehavior: .connectToPreviousIntentResult) + @Parameter( + title: "Image", + description: "Image to post on Mastodon", + supportedTypeIdentifiers: ["public.image"], + inputConnectionBehavior: .connectToPreviousIntentResult) var images: [IntentFile]? func perform() async throws -> some IntentResult { diff --git a/IceCubesAppIntents/TabIntent.swift b/IceCubesAppIntents/TabIntent.swift index 7f6fd107..55439a5f 100644 --- a/IceCubesAppIntents/TabIntent.swift +++ b/IceCubesAppIntents/TabIntent.swift @@ -17,22 +17,24 @@ enum TabEnum: String, AppEnum, Sendable { static let typeDisplayRepresentation: TypeDisplayRepresentation = "Tab" nonisolated static var caseDisplayRepresentations: [TabEnum: DisplayRepresentation] { - [.timeline: .init(title: "Home Timeline"), - .trending: .init(title: "Trending Timeline"), - .federated: .init(title: "Federated Timeline"), - .local: .init(title: "Local Timeline"), - .notifications: .init(title: "Notifications"), - .mentions: .init(title: "Mentions"), - .explore: .init(title: "Explore & Trending"), - .messages: .init(title: "Private Messages"), - .settings: .init(title: "Settings"), - .profile: .init(title: "Profile"), - .bookmarks: .init(title: "Bookmarks"), - .favorites: .init(title: "Favorites"), - .followedTags: .init(title: "Followed Tags"), - .lists: .init(title: "Lists"), - .links: .init(title: "Trending Links"), - .post: .init(title: "New post")] + [ + .timeline: .init(title: "Home Timeline"), + .trending: .init(title: "Trending Timeline"), + .federated: .init(title: "Federated Timeline"), + .local: .init(title: "Local Timeline"), + .notifications: .init(title: "Notifications"), + .mentions: .init(title: "Mentions"), + .explore: .init(title: "Explore & Trending"), + .messages: .init(title: "Private Messages"), + .settings: .init(title: "Settings"), + .profile: .init(title: "Profile"), + .bookmarks: .init(title: "Bookmarks"), + .favorites: .init(title: "Favorites"), + .followedTags: .init(title: "Followed Tags"), + .lists: .init(title: "Lists"), + .links: .init(title: "Trending Links"), + .post: .init(title: "New post"), + ] } var toAppTab: AppTab { diff --git a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift index 6b3f3cd1..d323c656 100644 --- a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift +++ b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift @@ -10,24 +10,30 @@ struct AccountWidgetProvider: AppIntentTimelineProvider { .init(date: Date(), account: .placeholder(), avatar: nil) } - func snapshot(for configuration: AccountWidgetConfiguration, in _: Context) async -> AccountWidgetEntry { + func snapshot(for configuration: AccountWidgetConfiguration, in _: Context) async + -> AccountWidgetEntry + { let account = await fetchAccount(configuration: configuration) return .init(date: Date(), account: account, avatar: nil) } - func timeline(for configuration: AccountWidgetConfiguration, in _: Context) async -> Timeline { + func timeline(for configuration: AccountWidgetConfiguration, in _: Context) async -> Timeline< + AccountWidgetEntry + > { let account = await fetchAccount(configuration: configuration) let images = try? await loadImages(urls: [account.avatar]) - return .init(entries: [.init(date: Date(), account: account, avatar: images?.first?.value)], - policy: .atEnd) + return .init( + entries: [.init(date: Date(), account: account, avatar: images?.first?.value)], + policy: .atEnd) } private func fetchAccount(configuration: AccountWidgetConfiguration) async -> Account { guard let account = configuration.account else { return .placeholder() } - let client = Client(server: account.account.server, - oauthToken: account.account.oauthToken) + let client = Client( + server: account.account.server, + oauthToken: account.account.oauthToken) do { let account: Account = try await client.get(endpoint: Accounts.verifyCredentials) return account @@ -47,10 +53,11 @@ struct AccountWidget: Widget { let kind: String = "AccountWidget" var body: some WidgetConfiguration { - AppIntentConfiguration(kind: kind, - intent: AccountWidgetConfiguration.self, - provider: AccountWidgetProvider()) - { entry in + AppIntentConfiguration( + kind: kind, + intent: AccountWidgetConfiguration.self, + provider: AccountWidgetProvider() + ) { entry in AccountWidgetView(entry: entry) .containerBackground(Color("WidgetBackground").gradient, for: .widget) } diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift index 42652ed8..2552c54d 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift @@ -7,50 +7,71 @@ import WidgetKit struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { func placeholder(in _: Context) -> PostsWidgetEntry { - .init(date: Date(), - title: "#Mastodon", - statuses: [.placeholder()], - images: [:]) + .init( + date: Date(), + title: "#Mastodon", + statuses: [.placeholder()], + images: [:]) } - func snapshot(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { + func snapshot(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async + -> PostsWidgetEntry + { if let entry = await timeline(for: configuration, context: context).entries.first { return entry } - return .init(date: Date(), - title: "#Mastodon", - statuses: [], - images: [:]) + return .init( + date: Date(), + title: "#Mastodon", + statuses: [], + images: [:]) } - func timeline(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> Timeline { + func timeline(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async + -> Timeline + { await timeline(for: configuration, context: context) } - private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline { + private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async + -> Timeline + { do { guard let account = configuration.account, let hashgtag = configuration.hashgtag else { - return Timeline(entries: [.init(date: Date(), - title: "#Mastodon", - statuses: [], - images: [:])], - policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: "#Mastodon", + statuses: [], + images: [:]) + ], + policy: .atEnd) } let timeline: TimelineFilter = .hashtag(tag: hashgtag, accountId: nil) - let statuses = await loadStatuses(for: timeline, - account: account, - widgetFamily: context.family) + let statuses = await loadStatuses( + for: timeline, + account: account, + widgetFamily: context.family) let images = try await loadImages(urls: statuses.map { $0.account.avatar }) - return Timeline(entries: [.init(date: Date(), - title: timeline.title, - statuses: statuses, - images: images)], policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: timeline.title, + statuses: statuses, + images: images) + ], policy: .atEnd) } catch { - return Timeline(entries: [.init(date: Date(), - title: "#Mastodon", - statuses: [], - images: [:])], - policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: "#Mastodon", + statuses: [], + images: [:]) + ], + policy: .atEnd) } } } @@ -59,10 +80,11 @@ struct HashtagPostsWidget: Widget { let kind: String = "HashtagPostsWidget" var body: some WidgetConfiguration { - AppIntentConfiguration(kind: kind, - intent: HashtagPostsWidgetConfiguration.self, - provider: HashtagPostsWidgetProvider()) - { entry in + AppIntentConfiguration( + kind: kind, + intent: HashtagPostsWidgetConfiguration.self, + provider: HashtagPostsWidgetProvider() + ) { entry in PostsWidgetView(entry: entry) .containerBackground(Color("WidgetBackground").gradient, for: .widget) } @@ -75,8 +97,9 @@ struct HashtagPostsWidget: Widget { #Preview(as: .systemMedium) { HashtagPostsWidget() } timeline: { - PostsWidgetEntry(date: .now, - title: "#Mastodon", - statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], - images: [:]) + PostsWidgetEntry( + date: .now, + title: "#Mastodon", + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) } diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift index b7df808e..7faac652 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift @@ -7,49 +7,70 @@ import WidgetKit struct LatestPostsWidgetProvider: AppIntentTimelineProvider { func placeholder(in _: Context) -> PostsWidgetEntry { - .init(date: Date(), - title: "Home", - statuses: [.placeholder()], - images: [:]) + .init( + date: Date(), + title: "Home", + statuses: [.placeholder()], + images: [:]) } - func snapshot(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { + func snapshot(for configuration: LatestPostsWidgetConfiguration, in context: Context) async + -> PostsWidgetEntry + { if let entry = await timeline(for: configuration, context: context).entries.first { return entry } - return .init(date: Date(), - title: configuration.timeline?.timeline.title ?? "", - statuses: [], - images: [:]) + return .init( + date: Date(), + title: configuration.timeline?.timeline.title ?? "", + statuses: [], + images: [:]) } - func timeline(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> Timeline { + func timeline(for configuration: LatestPostsWidgetConfiguration, in context: Context) async + -> Timeline + { await timeline(for: configuration, context: context) } - private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline { + private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async + -> Timeline + { do { guard let timeline = configuration.timeline, let account = configuration.account else { - return Timeline(entries: [.init(date: Date(), - title: "", - statuses: [], - images: [:])], - policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: "", + statuses: [], + images: [:]) + ], + policy: .atEnd) } - let statuses = await loadStatuses(for: timeline.timeline, - account: account, - widgetFamily: context.family) + let statuses = await loadStatuses( + for: timeline.timeline, + account: account, + widgetFamily: context.family) let images = try await loadImages(urls: statuses.map { $0.account.avatar }) - return Timeline(entries: [.init(date: Date(), - title: timeline.timeline.title, - statuses: statuses, - images: images)], policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: timeline.timeline.title, + statuses: statuses, + images: images) + ], policy: .atEnd) } catch { - return Timeline(entries: [.init(date: Date(), - title: configuration.timeline?.timeline.title ?? "", - statuses: [], - images: [:])], - policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: configuration.timeline?.timeline.title ?? "", + statuses: [], + images: [:]) + ], + policy: .atEnd) } } @@ -77,10 +98,11 @@ struct LatestPostsWidget: Widget { let kind: String = "LatestPostsWidget" var body: some WidgetConfiguration { - AppIntentConfiguration(kind: kind, - intent: LatestPostsWidgetConfiguration.self, - provider: LatestPostsWidgetProvider()) - { entry in + AppIntentConfiguration( + kind: kind, + intent: LatestPostsWidgetConfiguration.self, + provider: LatestPostsWidgetProvider() + ) { entry in PostsWidgetView(entry: entry) .containerBackground(Color("WidgetBackground").gradient, for: .widget) } @@ -93,8 +115,9 @@ struct LatestPostsWidget: Widget { #Preview(as: .systemMedium) { LatestPostsWidget() } timeline: { - PostsWidgetEntry(date: .now, - title: "Mastodon", - statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], - images: [:]) + PostsWidgetEntry( + date: .now, + title: "Mastodon", + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) } diff --git a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift index 42cebd3f..fb16f333 100644 --- a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift +++ b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift @@ -7,50 +7,71 @@ import WidgetKit struct ListsWidgetProvider: AppIntentTimelineProvider { func placeholder(in _: Context) -> PostsWidgetEntry { - .init(date: Date(), - title: "List name", - statuses: [.placeholder()], - images: [:]) + .init( + date: Date(), + title: "List name", + statuses: [.placeholder()], + images: [:]) } - func snapshot(for configuration: ListsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { + func snapshot(for configuration: ListsWidgetConfiguration, in context: Context) async + -> PostsWidgetEntry + { if let entry = await timeline(for: configuration, context: context).entries.first { return entry } - return .init(date: Date(), - title: "List name", - statuses: [], - images: [:]) + return .init( + date: Date(), + title: "List name", + statuses: [], + images: [:]) } - func timeline(for configuration: ListsWidgetConfiguration, in context: Context) async -> Timeline { + func timeline(for configuration: ListsWidgetConfiguration, in context: Context) async -> Timeline< + PostsWidgetEntry + > { await timeline(for: configuration, context: context) } - private func timeline(for configuration: ListsWidgetConfiguration, context: Context) async -> Timeline { + private func timeline(for configuration: ListsWidgetConfiguration, context: Context) async + -> Timeline + { do { guard let account = configuration.account, let timeline = configuration.timeline else { - return Timeline(entries: [.init(date: Date(), - title: "List name", - statuses: [], - images: [:])], - policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: "List name", + statuses: [], + images: [:]) + ], + policy: .atEnd) } let filter: TimelineFilter = .list(list: timeline.list) - let statuses = await loadStatuses(for: filter, - account: account, - widgetFamily: context.family) + let statuses = await loadStatuses( + for: filter, + account: account, + widgetFamily: context.family) let images = try await loadImages(urls: statuses.map { $0.account.avatar }) - return Timeline(entries: [.init(date: Date(), - title: filter.title, - statuses: statuses, - images: images)], policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: filter.title, + statuses: statuses, + images: images) + ], policy: .atEnd) } catch { - return Timeline(entries: [.init(date: Date(), - title: "List name", - statuses: [], - images: [:])], - policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: "List name", + statuses: [], + images: [:]) + ], + policy: .atEnd) } } } @@ -59,10 +80,11 @@ struct ListsPostWidget: Widget { let kind: String = "ListsPostWidget" var body: some WidgetConfiguration { - AppIntentConfiguration(kind: kind, - intent: ListsWidgetConfiguration.self, - provider: ListsWidgetProvider()) - { entry in + AppIntentConfiguration( + kind: kind, + intent: ListsWidgetConfiguration.self, + provider: ListsWidgetProvider() + ) { entry in PostsWidgetView(entry: entry) .containerBackground(Color("WidgetBackground").gradient, for: .widget) } @@ -75,8 +97,9 @@ struct ListsPostWidget: Widget { #Preview(as: .systemMedium) { ListsPostWidget() } timeline: { - PostsWidgetEntry(date: .now, - title: "List name", - statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], - images: [:]) + PostsWidgetEntry( + date: .now, + title: "List name", + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) } diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift index 500a7bd8..6d61236a 100644 --- a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift @@ -7,56 +7,79 @@ import WidgetKit struct MentionsWidgetProvider: AppIntentTimelineProvider { func placeholder(in _: Context) -> PostsWidgetEntry { - .init(date: Date(), - title: "Mentions", - statuses: [.placeholder()], - images: [:]) + .init( + date: Date(), + title: "Mentions", + statuses: [.placeholder()], + images: [:]) } - func snapshot(for configuration: MentionsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { + func snapshot(for configuration: MentionsWidgetConfiguration, in context: Context) async + -> PostsWidgetEntry + { if let entry = await timeline(for: configuration, context: context).entries.first { return entry } - return .init(date: Date(), - title: "Mentions", - statuses: [], - images: [:]) + return .init( + date: Date(), + title: "Mentions", + statuses: [], + images: [:]) } - func timeline(for configuration: MentionsWidgetConfiguration, in context: Context) async -> Timeline { + func timeline(for configuration: MentionsWidgetConfiguration, in context: Context) async + -> Timeline + { await timeline(for: configuration, context: context) } - private func timeline(for configuration: MentionsWidgetConfiguration, context _: Context) async -> Timeline { + private func timeline(for configuration: MentionsWidgetConfiguration, context _: Context) async + -> Timeline + { do { guard let account = configuration.account else { - return Timeline(entries: [.init(date: Date(), - title: "Mentions", - statuses: [], - images: [:])], - policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: "Mentions", + statuses: [], + images: [:]) + ], + policy: .atEnd) } - let client = Client(server: account.account.server, - oauthToken: account.account.oauthToken) + let client = Client( + server: account.account.server, + oauthToken: account.account.oauthToken) var excludedTypes = Models.Notification.NotificationType.allCases excludedTypes.removeAll(where: { $0 == .mention }) let notifications: [Models.Notification] = - try await client.get(endpoint: Notifications.notifications(minId: nil, - maxId: nil, - types: excludedTypes.map(\.rawValue), - limit: 5)) + try await client.get( + endpoint: Notifications.notifications( + minId: nil, + maxId: nil, + types: excludedTypes.map(\.rawValue), + limit: 5)) let statuses = notifications.compactMap { $0.status } let images = try await loadImages(urls: statuses.map { $0.account.avatar }) - return Timeline(entries: [.init(date: Date(), - title: "Mentions", - statuses: statuses, - images: images)], policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: "Mentions", + statuses: statuses, + images: images) + ], policy: .atEnd) } catch { - return Timeline(entries: [.init(date: Date(), - title: "Mentions", - statuses: [], - images: [:])], - policy: .atEnd) + return Timeline( + entries: [ + .init( + date: Date(), + title: "Mentions", + statuses: [], + images: [:]) + ], + policy: .atEnd) } } } @@ -65,10 +88,11 @@ struct MentionsWidget: Widget { let kind: String = "MentionsWidget" var body: some WidgetConfiguration { - AppIntentConfiguration(kind: kind, - intent: MentionsWidgetConfiguration.self, - provider: MentionsWidgetProvider()) - { entry in + AppIntentConfiguration( + kind: kind, + intent: MentionsWidgetConfiguration.self, + provider: MentionsWidgetProvider() + ) { entry in PostsWidgetView(entry: entry) .containerBackground(Color("WidgetBackground").gradient, for: .widget) } @@ -81,8 +105,9 @@ struct MentionsWidget: Widget { #Preview(as: .systemMedium) { MentionsWidget() } timeline: { - PostsWidgetEntry(date: .now, - title: "Mentions", - statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], - images: [:]) + PostsWidgetEntry( + date: .now, + title: "Mentions", + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) } diff --git a/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift index eeeeccf0..1be45323 100644 --- a/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift +++ b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift @@ -52,16 +52,18 @@ struct PostsWidgetView: View { @ViewBuilder private func makeStatusView(_ status: Status) -> some View { if let url = URL(string: status.url ?? "") { - Link(destination: url, label: { - VStack(alignment: .leading, spacing: 2) { - makeStatusHeaderView(status) - Text(status.content.asSafeMarkdownAttributedString) - .font(.footnote) - .lineLimit(contentLineLimit) - .fixedSize(horizontal: false, vertical: true) - .padding(.leading, 20) - } - }) + Link( + destination: url, + label: { + VStack(alignment: .leading, spacing: 2) { + makeStatusHeaderView(status) + Text(status.content.asSafeMarkdownAttributedString) + .font(.footnote) + .lineLimit(contentLineLimit) + .fixedSize(horizontal: false, vertical: true) + .padding(.leading, 20) + } + }) } } diff --git a/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift b/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift index 0e3c7a82..0a964b19 100644 --- a/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift +++ b/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift @@ -7,17 +7,20 @@ import Timeline import UIKit import WidgetKit -func loadStatuses(for timeline: TimelineFilter, - account: AppAccountEntity, - widgetFamily: WidgetFamily) async -> [Status] -{ +func loadStatuses( + for timeline: TimelineFilter, + account: AppAccountEntity, + widgetFamily: WidgetFamily +) async -> [Status] { let client = Client(server: account.account.server, oauthToken: account.account.oauthToken) do { - var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, - maxId: nil, - minId: nil, - offset: nil, - limit: 6)) + var statuses: [Status] = try await client.get( + endpoint: timeline.endpoint( + sinceId: nil, + maxId: nil, + minId: nil, + offset: nil, + limit: 6)) statuses = statuses.filter { $0.reblog == nil && !$0.content.asRawText.isEmpty } switch widgetFamily { case .systemSmall, .systemMedium: diff --git a/IceCubesNotifications/NotificationService.swift b/IceCubesNotifications/NotificationService.swift index c5abf884..4c0f0bb0 100644 --- a/IceCubesNotifications/NotificationService.swift +++ b/IceCubesNotifications/NotificationService.swift @@ -9,16 +9,19 @@ import Notifications import UIKit import UserNotifications -extension UNMutableNotificationContent: @unchecked @retroactive Sendable { } +extension UNMutableNotificationContent: @unchecked @retroactive Sendable {} class NotificationService: UNNotificationServiceExtension { - override func didReceive(_ request: UNNotificationRequest, - withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - + override func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) { + let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent let provider = NotificationServiceContentProvider(bestAttemptContent: bestAttemptContent) - let casted = unsafeBitCast(contentHandler, - to: (@Sendable (UNNotificationContent) -> Void).self) + let casted = unsafeBitCast( + contentHandler, + to: (@Sendable (UNNotificationContent) -> Void).self) Task { if let content = await provider.buildContent() { casted(content) @@ -29,65 +32,72 @@ class NotificationService: UNNotificationServiceExtension { actor NotificationServiceContentProvider { var bestAttemptContent: UNMutableNotificationContent? - + private let pushKeys = PushKeys() private let keychainAccounts = AppAccount.retrieveAll() - + init(bestAttemptContent: UNMutableNotificationContent? = nil) { self.bestAttemptContent = bestAttemptContent } - + func buildContent() async -> UNMutableNotificationContent? { if var bestAttemptContent { let privateKey = pushKeys.notificationsPrivateKeyAsKey let auth = pushKeys.notificationsAuthKeyAsKey - + guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String, - let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) + let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) else { return bestAttemptContent } - + guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String, - let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()), - let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) + let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()), + let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) else { return bestAttemptContent } - + guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String, - let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64()) + let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64()) else { return bestAttemptContent } - - guard let plaintextData = NotificationService.decrypt(payload: payload, - salt: salt, - auth: auth, - privateKey: privateKey, - publicKey: publicKey), - let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) + + guard + let plaintextData = NotificationService.decrypt( + payload: payload, + salt: salt, + auth: auth, + privateKey: privateKey, + publicKey: publicKey), + let notification = try? JSONDecoder().decode( + MastodonPushNotification.self, from: plaintextData) else { return bestAttemptContent } - + bestAttemptContent.title = notification.title if keychainAccounts.count > 1 { bestAttemptContent.subtitle = bestAttemptContent.userInfo["i"] as? String ?? "" } bestAttemptContent.body = notification.body.escape() bestAttemptContent.userInfo["plaintext"] = plaintextData - bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "glass.caf")) + bestAttemptContent.sound = UNNotificationSound( + named: UNNotificationSoundName(rawValue: "glass.caf")) let badgeCount = await updateBadgeCoung(notification: notification) bestAttemptContent.badge = .init(integerLiteral: badgeCount) - + if let urlString = notification.icon, - let url = URL(string: urlString) { - let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments") - try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) + let url = URL(string: urlString) + { + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("notification-attachments") + try? FileManager.default.createDirectory( + at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) let filename = url.lastPathComponent let fileURL = temporaryDirectoryURL.appendingPathComponent(filename) - + // Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated // context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor // boundary. @@ -96,20 +106,23 @@ actor NotificationServiceContentProvider { if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) { if let image = UIImage(data: data) { try? image.pngData()?.write(to: fileURL) - + if let remoteNotification = await toRemoteNotification(localNotification: notification), - let type = remoteNotification.supportedType + let type = remoteNotification.supportedType { - let intent = buildMessageIntent(remoteNotification: remoteNotification, - currentUser: bestAttemptContent.userInfo["i"] as? String ?? "", - avatarURL: fileURL) + let intent = buildMessageIntent( + remoteNotification: remoteNotification, + currentUser: bestAttemptContent.userInfo["i"] as? String ?? "", + avatarURL: fileURL) do { - bestAttemptContent = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent + bestAttemptContent = + try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent bestAttemptContent.threadIdentifier = remoteNotification.type if type == .mention { bestAttemptContent.body = notification.body.escape() } else { - let newBody = "\(NSLocalizedString(type.notificationKey(), bundle: .main, comment: ""))\(notification.body.escape())" + let newBody = + "\(NSLocalizedString(type.notificationKey(), bundle: .main, comment: ""))\(notification.body.escape())" bestAttemptContent.body = newBody } return bestAttemptContent @@ -117,9 +130,11 @@ actor NotificationServiceContentProvider { return bestAttemptContent } } else { - if let attachment = try? UNNotificationAttachment(identifier: filename, - url: fileURL, - options: nil) { + if let attachment = try? UNNotificationAttachment( + identifier: filename, + url: fileURL, + options: nil) + { bestAttemptContent.attachments = [attachment] } } @@ -133,13 +148,17 @@ actor NotificationServiceContentProvider { } return nil } - - - private func toRemoteNotification(localNotification: MastodonPushNotification) async -> Models.Notification? { + + private func toRemoteNotification(localNotification: MastodonPushNotification) async -> Models + .Notification? + { do { - if let account = keychainAccounts.first(where: { $0.oauthToken?.accessToken == localNotification.accessToken }) { + if let account = keychainAccounts.first(where: { + $0.oauthToken?.accessToken == localNotification.accessToken + }) { let client = Client(server: account.server, oauthToken: account.oauthToken) - let remoteNotification: Models.Notification = try await client.get(endpoint: Notifications.notification(id: String(localNotification.notificationID))) + let remoteNotification: Models.Notification = try await client.get( + endpoint: Notifications.notification(id: String(localNotification.notificationID))) return remoteNotification } } catch { @@ -147,52 +166,58 @@ actor NotificationServiceContentProvider { } return nil } - - private func buildMessageIntent(remoteNotification: Models.Notification, - currentUser: String, - avatarURL: URL) -> INSendMessageIntent - { + + private func buildMessageIntent( + remoteNotification: Models.Notification, + currentUser: String, + avatarURL: URL + ) -> INSendMessageIntent { let handle = INPersonHandle(value: remoteNotification.account.id, type: .unknown) let avatar = INImage(url: avatarURL) - let sender = INPerson(personHandle: handle, - nameComponents: nil, - displayName: remoteNotification.account.safeDisplayName, - image: avatar, - contactIdentifier: nil, - customIdentifier: nil) + let sender = INPerson( + personHandle: handle, + nameComponents: nil, + displayName: remoteNotification.account.safeDisplayName, + image: avatar, + contactIdentifier: nil, + customIdentifier: nil) var recipents: [INPerson]? var groupName: INSpeakableString? if keychainAccounts.count > 1 { - let me = INPerson(personHandle: .init(value: currentUser, type: .unknown), - nameComponents: nil, - displayName: currentUser, - image: nil, - contactIdentifier: nil, - customIdentifier: nil) + let me = INPerson( + personHandle: .init(value: currentUser, type: .unknown), + nameComponents: nil, + displayName: currentUser, + image: nil, + contactIdentifier: nil, + customIdentifier: nil) recipents = [me, sender] groupName = .init(spokenPhrase: currentUser) } - let intent = INSendMessageIntent(recipients: recipents, - outgoingMessageType: .outgoingMessageText, - content: nil, - speakableGroupName: groupName, - conversationIdentifier: remoteNotification.account.id, - serviceName: nil, - sender: sender, - attachments: nil) + let intent = INSendMessageIntent( + recipients: recipents, + outgoingMessageType: .outgoingMessageText, + content: nil, + speakableGroupName: groupName, + conversationIdentifier: remoteNotification.account.id, + serviceName: nil, + sender: sender, + attachments: nil) if groupName != nil { intent.setImage(avatar, forParameterNamed: \.speakableGroupName) } return intent } - + @MainActor private func updateBadgeCoung(notification: MastodonPushNotification) -> Int { let preferences = UserPreferences.shared let tokens = AppAccountsManager.shared.pushAccounts.map(\.token) preferences.reloadNotificationsCount(tokens: tokens) - - if let token = keychainAccounts.first(where: { $0.oauthToken?.accessToken == notification.accessToken })?.oauthToken { + + if let token = keychainAccounts.first(where: { + $0.oauthToken?.accessToken == notification.accessToken + })?.oauthToken { var currentCount = preferences.notificationsCount[token] ?? 0 currentCount += 1 preferences.notificationsCount[token] = currentCount diff --git a/IceCubesNotifications/NotificationServiceSupport.swift b/IceCubesNotifications/NotificationServiceSupport.swift index 889af66f..6920516e 100644 --- a/IceCubesNotifications/NotificationServiceSupport.swift +++ b/IceCubesNotifications/NotificationServiceSupport.swift @@ -2,18 +2,29 @@ import CryptoKit import Foundation extension NotificationService { - static func decrypt(payload: Data, salt: Data, auth: Data, privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) -> Data? { + static func decrypt( + payload: Data, salt: Data, auth: Data, privateKey: P256.KeyAgreement.PrivateKey, + publicKey: P256.KeyAgreement.PublicKey + ) -> Data? { guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: publicKey) else { return nil } - let keyMaterial = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: auth, sharedInfo: Data("Content-Encoding: auth\0".utf8), outputByteCount: 32) + let keyMaterial = sharedSecret.hkdfDerivedSymmetricKey( + using: SHA256.self, salt: auth, sharedInfo: Data("Content-Encoding: auth\0".utf8), + outputByteCount: 32) - let keyInfo = info(type: "aesgcm", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation) - let key = HKDF.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: keyInfo, outputByteCount: 16) + let keyInfo = info( + type: "aesgcm", clientPublicKey: privateKey.publicKey.x963Representation, + serverPublicKey: publicKey.x963Representation) + let key = HKDF.deriveKey( + inputKeyMaterial: keyMaterial, salt: salt, info: keyInfo, outputByteCount: 16) - let nonceInfo = info(type: "nonce", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation) - let nonce = HKDF.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12) + let nonceInfo = info( + type: "nonce", clientPublicKey: privateKey.publicKey.x963Representation, + serverPublicKey: publicKey.x963Representation) + let nonce = HKDF.deriveKey( + inputKeyMaterial: keyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12) let nonceData = nonce.withUnsafeBytes(Array.init) diff --git a/IceCubesShareExtension/ShareViewController.swift b/IceCubesShareExtension/ShareViewController.swift index 7e66786b..63db4d0e 100644 --- a/IceCubesShareExtension/ShareViewController.swift +++ b/IceCubesShareExtension/ShareViewController.swift @@ -59,10 +59,11 @@ class ShareViewController: UIViewController { } } - NotificationCenter.default.addObserver(forName: .shareSheetClose, - object: nil, - queue: nil) - { [weak self] _ in + NotificationCenter.default.addObserver( + forName: .shareSheetClose, + object: nil, + queue: nil + ) { [weak self] _ in self?.close() } } diff --git a/Packages/Account/Package.swift b/Packages/Account/Package.swift index eccfa71a..eb3b7538 100644 --- a/Packages/Account/Package.swift +++ b/Packages/Account/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "Account", targets: ["Account"] - ), + ) ], dependencies: [ .package(name: "Network", path: "../Network"), @@ -36,7 +36,7 @@ let package = Package( .product(name: "WrappingHStack", package: "WrappingHStack"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] ), .testTarget( diff --git a/Packages/Account/Sources/Account/AccountDetailContextMenu.swift b/Packages/Account/Sources/Account/AccountDetailContextMenu.swift index 9d193278..cb6290ab 100644 --- a/Packages/Account/Sources/Account/AccountDetailContextMenu.swift +++ b/Packages/Account/Sources/Account/AccountDetailContextMenu.swift @@ -18,8 +18,9 @@ public struct AccountDetailContextMenu: View { Section(account.acct) { if !viewModel.isCurrentUser { Button { - routerPath.presentedSheet = .mentionStatusEditor(account: account, - visibility: preferences.postVisibility) + routerPath.presentedSheet = .mentionStatusEditor( + account: account, + visibility: preferences.postVisibility) } label: { Label("account.action.mention", systemImage: "at") } @@ -37,11 +38,13 @@ public struct AccountDetailContextMenu: View { Button { Task { do { - viewModel.relationship = try await client.post(endpoint: Accounts.unblock(id: account.id)) + viewModel.relationship = try await client.post( + endpoint: Accounts.unblock(id: account.id)) } catch {} } } label: { - Label("account.action.unblock", systemImage: "person.crop.circle.badge.exclamationmark") + Label( + "account.action.unblock", systemImage: "person.crop.circle.badge.exclamationmark") } } else { Button { @@ -55,7 +58,8 @@ public struct AccountDetailContextMenu: View { Button { Task { do { - viewModel.relationship = try await client.post(endpoint: Accounts.unmute(id: account.id)) + viewModel.relationship = try await client.post( + endpoint: Accounts.unmute(id: account.id)) } catch {} } } label: { @@ -67,7 +71,9 @@ public struct AccountDetailContextMenu: View { Button(duration.description) { Task { do { - viewModel.relationship = try await client.post(endpoint: Accounts.mute(id: account.id, json: MuteData(duration: duration.rawValue))) + viewModel.relationship = try await client.post( + endpoint: Accounts.mute( + id: account.id, json: MuteData(duration: duration.rawValue))) } catch {} } } @@ -78,15 +84,17 @@ public struct AccountDetailContextMenu: View { } if let relationship = viewModel.relationship, - relationship.following + relationship.following { if relationship.notifying { Button { Task { do { - viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, - notify: false, - reblogs: relationship.showingReblogs)) + viewModel.relationship = try await client.post( + endpoint: Accounts.follow( + id: account.id, + notify: false, + reblogs: relationship.showingReblogs)) } catch {} } } label: { @@ -96,9 +104,11 @@ public struct AccountDetailContextMenu: View { Button { Task { do { - viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, - notify: true, - reblogs: relationship.showingReblogs)) + viewModel.relationship = try await client.post( + endpoint: Accounts.follow( + id: account.id, + notify: true, + reblogs: relationship.showingReblogs)) } catch {} } } label: { @@ -109,9 +119,11 @@ public struct AccountDetailContextMenu: View { Button { Task { do { - viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, - notify: relationship.notifying, - reblogs: false)) + viewModel.relationship = try await client.post( + endpoint: Accounts.follow( + id: account.id, + notify: relationship.notifying, + reblogs: false)) } catch {} } } label: { @@ -121,9 +133,11 @@ public struct AccountDetailContextMenu: View { Button { Task { do { - viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, - notify: relationship.notifying, - reblogs: true)) + viewModel.relationship = try await client.post( + endpoint: Accounts.follow( + id: account.id, + notify: relationship.notifying, + reblogs: true)) } catch {} } } label: { @@ -159,7 +173,9 @@ public struct AccountDetailContextMenu: View { ShareLink(item: url, subject: Text(account.safeDisplayName)) { Label("account.action.share", systemImage: "square.and.arrow.up") } - Button { UIApplication.shared.open(url) } label: { + Button { + UIApplication.shared.open(url) + } label: { Label("status.action.view-in-browser", systemImage: "safari") } } diff --git a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift index d64167ac..df641965 100644 --- a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift +++ b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift @@ -1,17 +1,17 @@ +import AppAccount import DesignSystem import EmojiText import Env import Models import NukeUI import SwiftUI -import AppAccount @MainActor struct AccountDetailHeaderView: View { enum Constants { static let headerHeight: CGFloat = 200 } - + @Environment(\.openWindow) private var openWindow @Environment(Theme.self) private var theme @Environment(QuickLook.self) private var quickLook @@ -27,7 +27,7 @@ struct AccountDetailHeaderView: View { var viewModel: AccountDetailViewModel let account: Account let scrollViewProxy: ScrollViewProxy? - + private let premiumTimer = Timer.publish(every: 5, on: .main, in: .common).autoconnect() @State private var shouldListenToPremiumTimer: Bool = false @State private var isTipSheetPresented: Bool = false @@ -54,13 +54,16 @@ struct AccountDetailHeaderView: View { Spacer() } .onChange(of: watcher.latestEvent?.id) { - if let latestEvent = watcher.latestEvent, let latestEvent = latestEvent as? StreamEventNotification { - if latestEvent.notification.account.id == viewModel.accountId || - latestEvent.notification.account.id == viewModel.premiumAccount?.id { + if let latestEvent = watcher.latestEvent, + let latestEvent = latestEvent as? StreamEventNotification + { + if latestEvent.notification.account.id == viewModel.accountId + || latestEvent.notification.account.id == viewModel.premiumAccount?.id + { Task { if viewModel.account?.isLinkedToPremiumAccount == true { await viewModel.fetchAccount() - } else{ + } else { try? await viewModel.followButtonViewModel?.refreshRelationship() } } @@ -72,7 +75,7 @@ struct AccountDetailHeaderView: View { Task { if viewModel.account?.isLinkedToPremiumAccount == true { await viewModel.fetchAccount() - } else{ + } else { try? await viewModel.followButtonViewModel?.refreshRelationship() } } @@ -105,7 +108,7 @@ struct AccountDetailHeaderView: View { } } #if !os(visionOS) - .background(theme.secondaryBackgroundColor) + .background(theme.secondaryBackgroundColor) #endif .frame(height: Constants.headerHeight) .onTapGesture { @@ -114,10 +117,11 @@ struct AccountDetailHeaderView: View { } let attachement = MediaAttachment.imageWith(url: account.header) #if targetEnvironment(macCatalyst) || os(visionOS) - openWindow(value: WindowDestinationMedia.mediaViewer( - attachments: [attachement], - selectedAttachment: attachement - )) + openWindow( + value: WindowDestinationMedia.mediaViewer( + attachments: [attachement], + selectedAttachment: attachement + )) #else quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement]) #endif @@ -139,8 +143,10 @@ struct AccountDetailHeaderView: View { .resizable() .frame(width: 25, height: 25) .foregroundColor(theme.tintColor) - .offset(x: theme.avatarShape == .circle ? 0 : 10, - y: theme.avatarShape == .circle ? 0 : -10) + .offset( + x: theme.avatarShape == .circle ? 0 : 10, + y: theme.avatarShape == .circle ? 0 : -10 + ) .accessibilityRemoveTraits(.isSelected) .accessibilityLabel("accessibility.tabs.profile.user-avatar.supporter.label") } @@ -151,10 +157,13 @@ struct AccountDetailHeaderView: View { } let attachement = MediaAttachment.imageWith(url: account.avatar) #if targetEnvironment(macCatalyst) || os(visionOS) - openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement], - selectedAttachment: attachement)) + openWindow( + value: WindowDestinationMedia.mediaViewer( + attachments: [attachement], + selectedAttachment: attachement)) #else - quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement]) + quickLook.prepareFor( + selectedMediaAttachment: attachement, mediaAttachments: [attachement]) #endif } .accessibilityElement(children: .combine) @@ -188,7 +197,8 @@ struct AccountDetailHeaderView: View { makeCustomInfoLabel( title: "account.followers", count: account.followersCount ?? 0, - needsBadge: currentAccount.account?.id == account.id && !currentAccount.followRequests.isEmpty + needsBadge: currentAccount.account?.id == account.id + && !currentAccount.followRequests.isEmpty ) } .accessibilityHint("accessibility.tabs.profile.follower-count.hint") @@ -244,7 +254,11 @@ struct AccountDetailHeaderView: View { .accessibilityRespondsToUserInteraction(false) movedToView joinedAtView - if viewModel.account?.isPremiumAccount == true && viewModel.relationship?.following == false || viewModel.account?.isLinkedToPremiumAccount == true && viewModel.premiumRelationship?.following == false { + if viewModel.account?.isPremiumAccount == true + && viewModel.relationship?.following == false + || viewModel.account?.isLinkedToPremiumAccount == true + && viewModel.premiumRelationship?.following == false + { subscribeButton } } @@ -262,7 +276,7 @@ struct AccountDetailHeaderView: View { } if let note = viewModel.relationship?.note, !note.isEmpty, - !viewModel.isCurrentUser + !viewModel.isCurrentUser { makeNoteView(note) } @@ -274,9 +288,12 @@ struct AccountDetailHeaderView: View { .emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset) .padding(.top, 8) .textSelection(.enabled) - .environment(\.openURL, OpenURLAction { url in - routerPath.handle(url: url) - }) + .environment( + \.openURL, + OpenURLAction { url in + routerPath.handle(url: url) + } + ) .accessibilityRespondsToUserInteraction(false) if let translation = viewModel.translation, !viewModel.isLoadingTranslation { @@ -284,9 +301,12 @@ struct AccountDetailHeaderView: View { VStack(alignment: .leading, spacing: 4) { Text(translation.content.asSafeMarkdownAttributedString) .font(.scaledBody) - Text(getLocalizedStringLabel(langCode: translation.detectedSourceLanguage, provider: translation.provider)) - .font(.footnote) - .foregroundStyle(.secondary) + Text( + getLocalizedStringLabel( + langCode: translation.detectedSourceLanguage, provider: translation.provider) + ) + .font(.footnote) + .foregroundStyle(.secondary) } } .fixedSize(horizontal: false, vertical: true) @@ -307,7 +327,9 @@ struct AccountDetailHeaderView: View { } } - private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) -> some View { + private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) + -> some View + { VStack { Text(count, format: .number.notation(.compactName)) .font(.scaledHeadline) @@ -344,27 +366,31 @@ struct AccountDetailHeaderView: View { .accessibilityElement(children: .combine) } } - + @ViewBuilder private var subscribeButton: some View { Button { if let subscription = viewModel.subClubUser?.subscription, - let accountName = appAccount.currentAccount.accountName, - let premiumUsername = account.premiumUsername, - let url = URL(string: "https://\(AppInfo.premiumInstance)/@\(premiumUsername)/subscribe?callback=icecubesapp://subclub&id=@\(accountName)&amount=\(subscription.unitAmount)¤cy=\(subscription.currency)&theme=\(colorScheme)") { + let accountName = appAccount.currentAccount.accountName, + let premiumUsername = account.premiumUsername, + let url = URL( + string: + "https://\(AppInfo.premiumInstance)/@\(premiumUsername)/subscribe?callback=icecubesapp://subclub&id=@\(accountName)&amount=\(subscription.unitAmount)¤cy=\(subscription.currency)&theme=\(colorScheme)" + ) + { openURL(url) } else { isTipSheetPresented = true shouldListenToPremiumTimer = true } - + Task { if viewModel.account?.isLinkedToPremiumAccount == true { try? await viewModel.followPremiumAccount() } try? await viewModel.followButtonViewModel?.follow() } - + } label: { if let subscription = viewModel.subClubUser?.subscription { Text("Subscribe \(subscription.formattedAmount) / month") @@ -401,9 +427,9 @@ struct AccountDetailHeaderView: View { Text(note) .frame(maxWidth: .infinity, alignment: .leading) .padding(8) - #if !os(visionOS) - .background(theme.secondaryBackgroundColor) - #endif + #if !os(visionOS) + .background(theme.secondaryBackgroundColor) + #endif .cornerRadius(4) .overlay( RoundedRectangle(cornerRadius: 4) @@ -433,10 +459,15 @@ struct AccountDetailHeaderView: View { .emojiText.size(Font.scaledBodyFont.emojiSize) .emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset) .foregroundColor(theme.tintColor) - .environment(\.openURL, OpenURLAction { url in - routerPath.handle(url: url) - }) - .accessibilityValue(field.verifiedAt != nil ? "accessibility.tabs.profile.fields.verified.label" : "") + .environment( + \.openURL, + OpenURLAction { url in + routerPath.handle(url: url) + } + ) + .accessibilityValue( + field.verifiedAt != nil + ? "accessibility.tabs.profile.fields.verified.label" : "") } .font(.scaledBody) if viewModel.fields.last != field { @@ -447,7 +478,9 @@ struct AccountDetailHeaderView: View { Spacer() } .accessibilityElement(children: .combine) - .modifier(ConditionalUserDefinedFieldAccessibilityActionModifier(field: field, routerPath: routerPath)) + .modifier( + ConditionalUserDefinedFieldAccessibilityActionModifier( + field: field, routerPath: routerPath)) } } .padding(8) @@ -458,11 +491,11 @@ struct AccountDetailHeaderView: View { #else .background(theme.secondaryBackgroundColor) #endif - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(.gray.opacity(0.35), lineWidth: 1) - ) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(.gray.opacity(0.35), lineWidth: 1) + ) } } } @@ -492,8 +525,9 @@ private struct ConditionalUserDefinedFieldAccessibilityActionModifier: ViewModif struct AccountDetailHeaderView_Previews: PreviewProvider { static var previews: some View { - AccountDetailHeaderView(viewModel: .init(account: .placeholder()), - account: .placeholder(), - scrollViewProxy: nil) + AccountDetailHeaderView( + viewModel: .init(account: .placeholder()), + account: .placeholder(), + scrollViewProxy: nil) } } diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index ee5ec31d..394d0bfd 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -52,8 +52,7 @@ public struct AccountDetailView: View { .applyAccountDetailsRowStyle(theme: theme) Picker("", selection: $viewModel.selectedTab) { - ForEach(viewModel.tabs, id: \.self) - { tab in + ForEach(viewModel.tabs, id: \.self) { tab in if tab == .boosts { Image("Rocket") .tag(tab) @@ -81,17 +80,20 @@ public struct AccountDetailView: View { } .onTapGesture { if let account = viewModel.account { - routerPath.navigate(to: .accountMediaGridView(account: account, - initialMediaStatuses: viewModel.statusesMedias)) + routerPath.navigate( + to: .accountMediaGridView( + account: account, + initialMediaStatuses: viewModel.statusesMedias)) } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } - StatusesListView(fetcher: viewModel, - client: client, - routerPath: routerPath) + StatusesListView( + fetcher: viewModel, + client: client, + routerPath: routerPath) } .environment(\.defaultMinListRowHeight, 0) .listStyle(.plain) @@ -133,7 +135,7 @@ public struct AccountDetailView: View { } .onChange(of: watcher.latestEvent?.id) { if let latestEvent = watcher.latestEvent, - viewModel.accountId == currentAccount.account?.id + viewModel.accountId == currentAccount.account?.id { viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount) } @@ -146,9 +148,12 @@ public struct AccountDetailView: View { } } } - .sheet(isPresented: $isEditingRelationshipNote, content: { - EditRelationshipNoteView(accountDetailViewModel: viewModel) - }) + .sheet( + isPresented: $isEditingRelationshipNote, + content: { + EditRelationshipNoteView(accountDetailViewModel: viewModel) + } + ) .edgesIgnoringSafeArea(.top) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -160,15 +165,18 @@ public struct AccountDetailView: View { private func makeHeaderView(proxy: ScrollViewProxy?) -> some View { switch viewModel.accountState { case .loading: - AccountDetailHeaderView(viewModel: viewModel, - account: .placeholder(), - scrollViewProxy: proxy) - .redacted(reason: .placeholder) - .allowsHitTesting(false) + AccountDetailHeaderView( + viewModel: viewModel, + account: .placeholder(), + scrollViewProxy: proxy + ) + .redacted(reason: .placeholder) + .allowsHitTesting(false) case let .data(account): - AccountDetailHeaderView(viewModel: viewModel, - account: account, - scrollViewProxy: proxy) + AccountDetailHeaderView( + viewModel: viewModel, + account: account, + scrollViewProxy: proxy) case let .error(error): Text("Error: \(error.localizedDescription)") } @@ -237,23 +245,27 @@ public struct AccountDetailView: View { .font(.scaledFootnote) .foregroundStyle(.secondary) .fontWeight(.semibold) - .listRowInsets(.init(top: 0, - leading: 12, - bottom: 0, - trailing: .layoutPadding)) + .listRowInsets( + .init( + top: 0, + leading: 12, + bottom: 0, + trailing: .layoutPadding) + ) .listRowSeparator(.hidden) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif ForEach(viewModel.pinned) { status in - StatusRowExternalView(viewModel: .init(status: status, client: client, routerPath: routerPath)) + StatusRowExternalView( + viewModel: .init(status: status, client: client, routerPath: routerPath)) } Rectangle() - #if os(visionOS) - .fill(Color.clear) - #else - .fill(theme.secondaryBackgroundColor) - #endif + #if os(visionOS) + .fill(Color.clear) + #else + .fill(theme.secondaryBackgroundColor) + #endif .frame(height: 12) .listRowInsets(.init()) .listRowSeparator(.hidden) @@ -278,10 +290,13 @@ public struct AccountDetailView: View { Button { if let account = viewModel.account { #if targetEnvironment(macCatalyst) || os(visionOS) - openWindow(value: WindowDestinationEditor.mentionStatusEditor(account: account, visibility: preferences.postVisibility)) + openWindow( + value: WindowDestinationEditor.mentionStatusEditor( + account: account, visibility: preferences.postVisibility)) #else - routerPath.presentedSheet = .mentionStatusEditor(account: account, - visibility: preferences.postVisibility) + routerPath.presentedSheet = .mentionStatusEditor( + account: account, + visibility: preferences.postVisibility) #endif } } label: { @@ -290,9 +305,10 @@ public struct AccountDetailView: View { } Menu { - AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, - showTranslateView: $showTranslateView, - viewModel: viewModel) + AccountDetailContextMenu( + showBlockConfirmation: $showBlockConfirmation, + showTranslateView: $showTranslateView, + viewModel: viewModel) if !viewModel.isCurrentUser { Button { @@ -349,7 +365,10 @@ public struct AccountDetailView: View { Divider() Button { - if let url = URL(string: "https://mastometrics.com/auth/login?username=\(account.acct)@\(client.server)&instance=\(client.server)&auto=true") { + if let url = URL( + string: + "https://mastometrics.com/auth/login?username=\(account.acct)@\(client.server)&instance=\(client.server)&auto=true" + ) { openURL(url) } } label: { @@ -379,7 +398,8 @@ public struct AccountDetailView: View { Button("account.action.block-user-\(account.username)", role: .destructive) { Task { do { - viewModel.relationship = try await client.post(endpoint: Accounts.block(id: account.id)) + viewModel.relationship = try await client.post( + endpoint: Accounts.block(id: account.id)) } catch {} } } @@ -388,7 +408,8 @@ public struct AccountDetailView: View { Text("account.action.block-user-confirmation") } #if canImport(_Translation_SwiftUI) - .addTranslateView(isPresented: $showTranslateView, text: viewModel.account?.note.asRawText ?? "") + .addTranslateView( + isPresented: $showTranslateView, text: viewModel.account?.note.asRawText ?? "") #endif } } @@ -399,9 +420,9 @@ extension View { func applyAccountDetailsRowStyle(theme: Theme) -> some View { listRowInsets(.init()) .listRowSeparator(.hidden) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif } } diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index d77584cd..274a3ccb 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -12,7 +12,9 @@ import SwiftUI var isCurrentUser: Bool = false enum AccountState { - case loading, data(account: Account), error(error: Error) + case loading + case data(account: Account) + case error(error: Error) } enum Tab: Int { @@ -25,7 +27,7 @@ import SwiftUI static var accountTabs: [Tab] { [.statuses, .replies, .boosts, .media] } - + static var premiumAccountTabs: [Tab] { [.statuses, .premiumPosts, .replies, .boosts, .media] } @@ -54,8 +56,7 @@ import SwiftUI } } } - - + var tabs: [Tab] { if isCurrentUser { return Tab.currentAccountTabs @@ -78,13 +79,13 @@ import SwiftUI var featuredTags: [FeaturedTag] = [] var fields: [Account.Field] = [] var familiarFollowers: [Account] = [] - + // Sub.club stuff var premiumAccount: Account? var premiumRelationship: Relationship? var subClubUser: SubClubUser? private let subClubClient = SubClubClient() - + var selectedTab = Tab.statuses { didSet { switch selectedTab { @@ -103,9 +104,9 @@ import SwiftUI var translation: Translation? var isLoadingTranslation = false - + var followButtonViewModel: FollowButtonViewModel? - + private(set) var account: Account? private var tabTask: Task? @@ -139,7 +140,7 @@ import SwiftUI guard let client else { return } do { let data = try await fetchAccountData(accountId: accountId, client: client) - + accountState = .data(account: data.account) try await fetchPremiumAccount(fromAccount: data.account, client: client) account = data.account @@ -151,13 +152,14 @@ import SwiftUI if let followButtonViewModel { followButtonViewModel.relationship = relationship } else { - followButtonViewModel = .init(client: client, - accountId: accountId, - relationship: relationship, - shouldDisplayNotify: true, - relationshipUpdated: { [weak self] relationship in - self?.relationship = relationship - }) + followButtonViewModel = .init( + client: client, + accountId: accountId, + relationship: relationship, + shouldDisplayNotify: true, + relationshipUpdated: { [weak self] relationship in + self?.relationship = relationship + }) } } } catch { @@ -171,26 +173,32 @@ import SwiftUI private func fetchAccountData(accountId: String, client: Client) async throws -> AccountData { async let account: Account = client.get(endpoint: Accounts.accounts(id: accountId)) - async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId)) + async let featuredTags: [FeaturedTag] = client.get( + endpoint: Accounts.featuredTags(id: accountId)) if client.isAuth, !isCurrentUser { - async let relationships: [Relationship] = client.get(endpoint: Accounts.relationships(ids: [accountId])) + async let relationships: [Relationship] = client.get( + endpoint: Accounts.relationships(ids: [accountId])) do { - return try await .init(account: account, - featuredTags: featuredTags, - relationships: relationships) + return try await .init( + account: account, + featuredTags: featuredTags, + relationships: relationships) } catch { - return try await .init(account: account, - featuredTags: [], - relationships: relationships) + return try await .init( + account: account, + featuredTags: [], + relationships: relationships) } } - return try await .init(account: account, - featuredTags: featuredTags, - relationships: []) + return try await .init( + account: account, + featuredTags: featuredTags, + relationships: []) } func fetchFamilliarFollowers() async { - let familiarFollowers: [FamiliarAccounts]? = try? await client?.get(endpoint: Accounts.familiarFollowers(withAccount: accountId)) + let familiarFollowers: [FamiliarAccounts]? = try? await client?.get( + endpoint: Accounts.familiarFollowers(withAccount: accountId)) self.familiarFollowers = familiarFollowers?.first?.accounts ?? [] } @@ -204,31 +212,37 @@ import SwiftUI accountIdToFetch = accountId } statuses = - try await client.get(endpoint: Accounts.statuses(id: accountIdToFetch, - sinceId: nil, - tag: nil, - onlyMedia: selectedTab == .media, - excludeReplies: selectedTab != .replies, - excludeReblogs: selectedTab != .boosts, - pinned: nil)) + try await client.get( + endpoint: Accounts.statuses( + id: accountIdToFetch, + sinceId: nil, + tag: nil, + onlyMedia: selectedTab == .media, + excludeReplies: selectedTab != .replies, + excludeReblogs: selectedTab != .boosts, + pinned: nil)) StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client) if selectedTab == .boosts { boosts = statuses.filter { $0.reblog != nil } } if selectedTab == .statuses { pinned = - try await client.get(endpoint: Accounts.statuses(id: accountId, - sinceId: nil, - tag: nil, - onlyMedia: false, - excludeReplies: false, - excludeReblogs: false, - pinned: true)) + try await client.get( + endpoint: Accounts.statuses( + id: accountId, + sinceId: nil, + tag: nil, + onlyMedia: false, + excludeReplies: false, + excludeReblogs: false, + pinned: true)) StatusDataControllerProvider.shared.updateDataControllers(for: pinned, client: client) } if isCurrentUser { - (favorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nil)) - (bookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nil)) + (favorites, favoritesNextPage) = try await client.getWithLink( + endpoint: Accounts.favorites(sinceId: nil)) + (bookmarks, bookmarksNextPage) = try await client.getWithLink( + endpoint: Accounts.bookmarks(sinceId: nil)) StatusDataControllerProvider.shared.updateDataControllers(for: favorites, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: bookmarks, client: client) } @@ -248,13 +262,15 @@ import SwiftUI accountIdToFetch = accountId } let newStatuses: [Status] = - try await client.get(endpoint: Accounts.statuses(id: accountIdToFetch, - sinceId: lastId, - tag: nil, - onlyMedia: selectedTab == .media, - excludeReplies: selectedTab != .replies, - excludeReblogs: selectedTab != .boosts, - pinned: nil)) + try await client.get( + endpoint: Accounts.statuses( + id: accountIdToFetch, + sinceId: lastId, + tag: nil, + onlyMedia: selectedTab == .media, + excludeReplies: selectedTab != .replies, + excludeReblogs: selectedTab != .boosts, + pinned: nil)) statuses.append(contentsOf: newStatuses) if selectedTab == .boosts { let newBoosts = statuses.filter { $0.reblog != nil } @@ -262,23 +278,27 @@ import SwiftUI } StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client) if selectedTab == .boosts { - statusesState = .display(statuses: boosts, - nextPageState: newStatuses.count < 20 ? .none : .hasNextPage) + statusesState = .display( + statuses: boosts, + nextPageState: newStatuses.count < 20 ? .none : .hasNextPage) } else { - statusesState = .display(statuses: statuses, - nextPageState: newStatuses.count < 20 ? .none : .hasNextPage) + statusesState = .display( + statuses: statuses, + nextPageState: newStatuses.count < 20 ? .none : .hasNextPage) } case .favorites: guard let nextPageId = favoritesNextPage?.maxId else { return } let newFavorites: [Status] - (newFavorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nextPageId)) + (newFavorites, favoritesNextPage) = try await client.getWithLink( + endpoint: Accounts.favorites(sinceId: nextPageId)) favorites.append(contentsOf: newFavorites) StatusDataControllerProvider.shared.updateDataControllers(for: newFavorites, client: client) statusesState = .display(statuses: favorites, nextPageState: .hasNextPage) case .bookmarks: guard let nextPageId = bookmarksNextPage?.maxId else { return } let newBookmarks: [Status] - (newBookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nextPageId)) + (newBookmarks, bookmarksNextPage) = try await client.getWithLink( + endpoint: Accounts.bookmarks(sinceId: nextPageId)) StatusDataControllerProvider.shared.updateDataControllers(for: newBookmarks, client: client) bookmarks.append(contentsOf: newBookmarks) statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage) @@ -288,23 +308,27 @@ import SwiftUI private func reloadTabState() { switch selectedTab { case .statuses, .replies, .media, .premiumPosts: - statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage) + statusesState = .display( + statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage) case .boosts: - statusesState = .display(statuses: boosts, nextPageState: statuses.count < 20 ? .none : .hasNextPage) + statusesState = .display( + statuses: boosts, nextPageState: statuses.count < 20 ? .none : .hasNextPage) case .favorites: - statusesState = .display(statuses: favorites, - nextPageState: favoritesNextPage != nil ? .hasNextPage : .none) + statusesState = .display( + statuses: favorites, + nextPageState: favoritesNextPage != nil ? .hasNextPage : .none) case .bookmarks: - statusesState = .display(statuses: bookmarks, - nextPageState: bookmarksNextPage != nil ? .hasNextPage : .none) + statusesState = .display( + statuses: bookmarks, + nextPageState: bookmarksNextPage != nil ? .hasNextPage : .none) } } func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) { if let event = event as? StreamEventUpdate { if event.status.account.id == currentAccount.account?.id { - if (event.status.inReplyToId == nil && selectedTab == .statuses) || - (event.status.inReplyToId != nil && selectedTab == .replies) + if (event.status.inReplyToId == nil && selectedTab == .statuses) + || (event.status.inReplyToId != nil && selectedTab == .replies) { statuses.insert(event.status, at: 0) statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) @@ -329,30 +353,35 @@ import SwiftUI extension AccountDetailViewModel { private func fetchPremiumAccount(fromAccount: Account, client: Client) async throws { if fromAccount.isLinkedToPremiumAccount, let acct = fromAccount.premiumAcct { - let results: SearchResults? = try await client.get(endpoint: Search.search(query: acct, - type: .accounts, - offset: nil, - following: nil), - forceVersion: .v2) + let results: SearchResults? = try await client.get( + endpoint: Search.search( + query: acct, + type: .accounts, + offset: nil, + following: nil), + forceVersion: .v2) if let premiumAccount = results?.accounts.first { self.premiumAccount = premiumAccount await fetchSubClubAccount(premiumUsername: premiumAccount.username) - let relationships: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [premiumAccount.id])) + let relationships: [Relationship] = try await client.get( + endpoint: Accounts.relationships(ids: [premiumAccount.id])) self.premiumRelationship = relationships.first } } else if fromAccount.isPremiumAccount { await fetchSubClubAccount(premiumUsername: fromAccount.username) } } - + func followPremiumAccount() async throws { if let premiumAccount { - premiumRelationship = try await client?.post(endpoint: Accounts.follow(id: premiumAccount.id, - notify: false, - reblogs: true)) + premiumRelationship = try await client?.post( + endpoint: Accounts.follow( + id: premiumAccount.id, + notify: false, + reblogs: true)) } } - + private func fetchSubClubAccount(premiumUsername: String) async { let user = await subClubClient.getUser(username: premiumUsername) subClubUser = user diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift index 32299e08..6557e0ff 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift @@ -37,7 +37,10 @@ public struct AccountsListRow: View { let isFollowRequest: Bool let requestUpdated: (() -> Void)? - public init(viewModel: AccountsListRowViewModel, isFollowRequest: Bool = false, requestUpdated: (() -> Void)? = nil) { + public init( + viewModel: AccountsListRowViewModel, isFollowRequest: Bool = false, + requestUpdated: (() -> Void)? = nil + ) { self.viewModel = viewModel self.isFollowRequest = isFollowRequest self.requestUpdated = requestUpdated @@ -47,19 +50,23 @@ public struct AccountsListRow: View { HStack(alignment: .top) { AvatarView(viewModel.account.avatar) VStack(alignment: .leading, spacing: 2) { - EmojiTextApp(.init(stringValue: viewModel.account.safeDisplayName), emojis: viewModel.account.emojis) - .font(.scaledSubheadline) - .emojiText.size(Font.scaledSubheadlineFont.emojiSize) - .emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset) - .fontWeight(.semibold) + EmojiTextApp( + .init(stringValue: viewModel.account.safeDisplayName), emojis: viewModel.account.emojis + ) + .font(.scaledSubheadline) + .emojiText.size(Font.scaledSubheadlineFont.emojiSize) + .emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset) + .fontWeight(.semibold) Text("@\(viewModel.account.acct)") .font(.scaledFootnote) .foregroundStyle(Color.secondary) // First parameter is the number for the plural // Second parameter is the formatted string to show - Text("account.label.followers \(viewModel.account.followersCount ?? 0) \(viewModel.account.followersCount ?? 0, format: .number.notation(.compactName))") - .font(.scaledFootnote) + Text( + "account.label.followers \(viewModel.account.followersCount ?? 0) \(viewModel.account.followersCount ?? 0, format: .number.notation(.compactName))" + ) + .font(.scaledFootnote) if let field = viewModel.account.fields.filter({ $0.verifiedAt != nil }).first { HStack(spacing: 2) { @@ -71,9 +78,11 @@ public struct AccountsListRow: View { .font(.scaledFootnote) .emojiText.size(Font.scaledFootnoteFont.emojiSize) .emojiText.baselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset) - .environment(\.openURL, OpenURLAction { url in - routerPath.handle(url: url) - }) + .environment( + \.openURL, + OpenURLAction { url in + routerPath.handle(url: url) + }) } } @@ -81,25 +90,30 @@ public struct AccountsListRow: View { .font(.scaledCaption) .emojiText.size(Font.scaledFootnoteFont.emojiSize) .emojiText.baselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset) - .environment(\.openURL, OpenURLAction { url in - routerPath.handle(url: url) - }) + .environment( + \.openURL, + OpenURLAction { url in + routerPath.handle(url: url) + }) if isFollowRequest { - FollowRequestButtons(account: viewModel.account, - requestUpdated: requestUpdated) + FollowRequestButtons( + account: viewModel.account, + requestUpdated: requestUpdated) } } Spacer() if currentAccount.account?.id != viewModel.account.id, - let relationShip = viewModel.relationShip + let relationShip = viewModel.relationShip { VStack(alignment: .center) { - FollowButton(viewModel: .init(client: client, - accountId: viewModel.account.id, - relationship: relationShip, - shouldDisplayNotify: false, - relationshipUpdated: { _ in })) + FollowButton( + viewModel: .init( + client: client, + accountId: viewModel.account.id, + relationship: relationShip, + shouldDisplayNotify: false, + relationshipUpdated: { _ in })) } } } @@ -111,18 +125,21 @@ public struct AccountsListRow: View { routerPath.navigate(to: .accountDetailWithAccount(account: viewModel.account)) } #if canImport(_Translation_SwiftUI) - .addTranslateView(isPresented: $showTranslateView, text: viewModel.account.note.asRawText) + .addTranslateView(isPresented: $showTranslateView, text: viewModel.account.note.asRawText) #endif .contextMenu { - AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, - showTranslateView: $showTranslateView, - viewModel: .init(account: viewModel.account)) + AccountDetailContextMenu( + showBlockConfirmation: $showBlockConfirmation, + showTranslateView: $showTranslateView, + viewModel: .init(account: viewModel.account)) } preview: { List { - AccountDetailHeaderView(viewModel: .init(account: viewModel.account), - account: viewModel.account, - scrollViewProxy: nil) - .applyAccountDetailsRowStyle(theme: theme) + AccountDetailHeaderView( + viewModel: .init(account: viewModel.account), + account: viewModel.account, + scrollViewProxy: nil + ) + .applyAccountDetailsRowStyle(theme: theme) } .listStyle(.plain) .scrollContentBackground(.hidden) diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift index c8b794e0..b69596cd 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift @@ -18,32 +18,32 @@ public struct AccountsListView: View { public var body: some View { listView - #if !os(visionOS) - .scrollContentBackground(.hidden) - .background(theme.primaryBackgroundColor) - #endif - .listStyle(.plain) - .toolbar { - ToolbarItem(placement: .principal) { - VStack { - Text(viewModel.mode.title) - .font(.headline) - if let count = viewModel.totalCount { - Text(String(count)) - .font(.footnote) - .foregroundStyle(.secondary) + #if !os(visionOS) + .scrollContentBackground(.hidden) + .background(theme.primaryBackgroundColor) + #endif + .listStyle(.plain) + .toolbar { + ToolbarItem(placement: .principal) { + VStack { + Text(viewModel.mode.title) + .font(.headline) + if let count = viewModel.totalCount { + Text(String(count)) + .font(.footnote) + .foregroundStyle(.secondary) + } } } } - } - .navigationTitle(viewModel.mode.title) - .navigationBarTitleDisplayMode(.inline) - .task { - viewModel.client = client - guard !didAppear else { return } - didAppear = true - await viewModel.fetch() - } + .navigationTitle(viewModel.mode.title) + .navigationBarTitleDisplayMode(.inline) + .task { + viewModel.client = client + guard !didAppear else { return } + didAppear = true + await viewModel.fetch() + } } @ViewBuilder @@ -62,8 +62,10 @@ public struct AccountsListView: View { List { listContent } - .searchable(text: $viewModel.searchQuery, - placement: .navigationBarDrawer(displayMode: .always)) + .searchable( + text: $viewModel.searchQuery, + placement: .navigationBarDrawer(displayMode: .always) + ) .task(id: viewModel.searchQuery) { if !viewModel.searchQuery.isEmpty { await viewModel.search() @@ -92,13 +94,13 @@ public struct AccountsListView: View { AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder())) .redacted(reason: .placeholder) .allowsHitTesting(false) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif } case let .display(accounts, relationships, nextPageState): if case .followers = viewModel.mode, - !currentAccount.followRequests.isEmpty + !currentAccount.followRequests.isEmpty { Section( header: Text("account.follow-requests.pending-requests"), @@ -118,28 +120,32 @@ public struct AccountsListView: View { } ) #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } } } Section { if accounts.isEmpty { - PlaceholderView(iconName: "person.icloud", - title: "No accounts found", - message: "This list of accounts is empty") - .listRowSeparator(.hidden) + PlaceholderView( + iconName: "person.icloud", + title: "No accounts found", + message: "This list of accounts is empty" + ) + .listRowSeparator(.hidden) } else { ForEach(accounts) { account in if let relationship = relationships.first(where: { $0.id == account.id }) { - AccountsListRow(viewModel: .init(account: account, - relationShip: relationship)) + AccountsListRow( + viewModel: .init( + account: account, + relationShip: relationship)) } } } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif switch nextPageState { @@ -148,7 +154,7 @@ public struct AccountsListView: View { try await viewModel.fetchNextPage() } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif case .none: @@ -157,17 +163,19 @@ public struct AccountsListView: View { case let .error(error): Text(error.localizedDescription) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif } } } #Preview { List { - AccountsListRow(viewModel: .init(account: .placeholder(), - relationShip: .placeholder())) + AccountsListRow( + viewModel: .init( + account: .placeholder(), + relationShip: .placeholder())) } .listStyle(.plain) .withPreviewsEnv() diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift index 33c1b9e0..af142d7b 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift @@ -1,12 +1,14 @@ import Models import Network -import Observation import OSLog +import Observation import SwiftUI public enum AccountsListMode { - case following(accountId: String), followers(accountId: String) - case favoritedBy(statusId: String), rebloggedBy(statusId: String) + case following(accountId: String) + case followers(accountId: String) + case favoritedBy(statusId: String) + case rebloggedBy(statusId: String) case accountsList(accounts: [Account]) case blocked, muted @@ -42,9 +44,10 @@ public enum AccountsListMode { } case loading - case display(accounts: [Account], - relationships: [Relationship], - nextPageState: PagingState) + case display( + accounts: [Account], + relationships: [Relationship], + nextPageState: PagingState) case error(error: Error) } @@ -72,20 +75,28 @@ public enum AccountsListMode { case let .followers(accountId): let account: Account = try await client.get(endpoint: Accounts.accounts(id: accountId)) totalCount = account.followersCount - (accounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId, - maxId: nil)) + (accounts, link) = try await client.getWithLink( + endpoint: Accounts.followers( + id: accountId, + maxId: nil)) case let .following(accountId): self.accountId = accountId let account: Account = try await client.get(endpoint: Accounts.accounts(id: accountId)) totalCount = account.followingCount - (accounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId, - maxId: nil)) + (accounts, link) = try await client.getWithLink( + endpoint: Accounts.following( + id: accountId, + maxId: nil)) case let .rebloggedBy(statusId): - (accounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId, - maxId: nil)) + (accounts, link) = try await client.getWithLink( + endpoint: Statuses.rebloggedBy( + id: statusId, + maxId: nil)) case let .favoritedBy(statusId): - (accounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId, - maxId: nil)) + (accounts, link) = try await client.getWithLink( + endpoint: Statuses.favoritedBy( + id: statusId, + maxId: nil)) case let .accountsList(accounts): self.accounts = accounts link = nil @@ -97,11 +108,13 @@ public enum AccountsListMode { (accounts, link) = try await client.getWithLink(endpoint: Accounts.muteList) } nextPageId = link?.maxId - relationships = try await client.get(endpoint: - Accounts.relationships(ids: accounts.map(\.id))) - state = .display(accounts: accounts, - relationships: relationships, - nextPageState: link?.maxId != nil ? .hasNextPage : .none) + relationships = try await client.get( + endpoint: + Accounts.relationships(ids: accounts.map(\.id))) + state = .display( + accounts: accounts, + relationships: relationships, + nextPageState: link?.maxId != nil ? .hasNextPage : .none) } catch {} } @@ -111,17 +124,25 @@ public enum AccountsListMode { let link: LinkHandler? switch mode { case let .followers(accountId): - (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId, - maxId: nextPageId)) + (newAccounts, link) = try await client.getWithLink( + endpoint: Accounts.followers( + id: accountId, + maxId: nextPageId)) case let .following(accountId): - (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId, - maxId: nextPageId)) + (newAccounts, link) = try await client.getWithLink( + endpoint: Accounts.following( + id: accountId, + maxId: nextPageId)) case let .rebloggedBy(statusId): - (newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId, - maxId: nextPageId)) + (newAccounts, link) = try await client.getWithLink( + endpoint: Statuses.rebloggedBy( + id: statusId, + maxId: nextPageId)) case let .favoritedBy(statusId): - (newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId, - maxId: nextPageId)) + (newAccounts, link) = try await client.getWithLink( + endpoint: Statuses.favoritedBy( + id: statusId, + maxId: nextPageId)) case .accountsList: newAccounts = [] link = nil @@ -139,9 +160,10 @@ public enum AccountsListMode { relationships.append(contentsOf: newRelationships) self.nextPageId = link?.maxId - state = .display(accounts: accounts, - relationships: relationships, - nextPageState: link?.maxId != nil ? .hasNextPage : .none) + state = .display( + accounts: accounts, + relationships: relationships, + nextPageState: link?.maxId != nil ? .hasNextPage : .none) } func search() async { @@ -149,18 +171,21 @@ public enum AccountsListMode { do { state = .loading try await Task.sleep(for: .milliseconds(250)) - var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery, - type: .accounts, - offset: nil, - following: true), - forceVersion: .v2) + var results: SearchResults = try await client.get( + endpoint: Search.search( + query: searchQuery, + type: .accounts, + offset: nil, + following: true), + forceVersion: .v2) let relationships: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map(\.id))) results.relationships = relationships withAnimation { - state = .display(accounts: results.accounts, - relationships: relationships, - nextPageState: .none) + state = .display( + accounts: results.accounts, + relationships: relationships, + nextPageState: .none) } } catch {} } diff --git a/Packages/Account/Sources/Account/Edit/EditAccountView.swift b/Packages/Account/Sources/Account/Edit/EditAccountView.swift index 8904d019..15916fa1 100644 --- a/Packages/Account/Sources/Account/Edit/EditAccountView.swift +++ b/Packages/Account/Sources/Account/Edit/EditAccountView.swift @@ -35,20 +35,22 @@ public struct EditAccountView: View { .background(theme.secondaryBackgroundColor) .scrollDismissesKeyboard(.immediately) #endif - .navigationTitle("account.edit.navigation-title") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - toolbarContent - } - .alert("account.edit.error.save.title", - isPresented: $viewModel.saveError, - actions: { - Button("alert.button.ok", action: {}) - }, message: { Text("account.edit.error.save.message") }) - .task { - viewModel.client = client - await viewModel.fetchAccount() - } + .navigationTitle("account.edit.navigation-title") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + toolbarContent + } + .alert( + "account.edit.error.save.title", + isPresented: $viewModel.saveError, + actions: { + Button("alert.button.ok", action: {}) + }, message: { Text("account.edit.error.save.message") } + ) + .task { + viewModel.client = client + await viewModel.fetchAccount() + } } } @@ -61,7 +63,7 @@ public struct EditAccountView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -138,11 +140,12 @@ public struct EditAccountView: View { .listRowInsets(EdgeInsets()) } .listRowBackground(theme.secondaryBackgroundColor) - .photosPicker(isPresented: $viewModel.isPhotoPickerPresented, - selection: $viewModel.mediaPickers, - maxSelectionCount: 1, - matching: .any(of: [.images]), - photoLibrary: .shared()) + .photosPicker( + isPresented: $viewModel.isPhotoPickerPresented, + selection: $viewModel.mediaPickers, + maxSelectionCount: 1, + matching: .any(of: [.images]), + photoLibrary: .shared()) } @ViewBuilder @@ -156,7 +159,7 @@ public struct EditAccountView: View { .frame(maxHeight: 150) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -178,7 +181,7 @@ public struct EditAccountView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -188,14 +191,16 @@ public struct EditAccountView: View { Label("account.edit.account-settings.private", systemImage: "lock") } Toggle(isOn: $viewModel.isBot) { - Label("account.edit.account-settings.bot", systemImage: "laptopcomputer.trianglebadge.exclamationmark") + Label( + "account.edit.account-settings.bot", + systemImage: "laptopcomputer.trianglebadge.exclamationmark") } Toggle(isOn: $viewModel.isDiscoverable) { Label("account.edit.account-settings.discoverable", systemImage: "magnifyingglass") } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -231,7 +236,7 @@ public struct EditAccountView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } diff --git a/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift b/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift index 715c4f0d..24065f3d 100644 --- a/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift +++ b/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift @@ -94,13 +94,14 @@ import SwiftUI func save() async { isSaving = true do { - let data = UpdateCredentialsData(displayName: displayName, - note: note, - source: .init(privacy: postPrivacy, sensitive: isSensitive), - bot: isBot, - locked: isLocked, - discoverable: isDiscoverable, - fieldsAttributes: fields.map { .init(name: $0.name, value: $0.value) }) + let data = UpdateCredentialsData( + displayName: displayName, + note: note, + source: .init(privacy: postPrivacy, sensitive: isSensitive), + bot: isBot, + locked: isLocked, + discoverable: isDiscoverable, + fieldsAttributes: fields.map { .init(name: $0.name, value: $0.value) }) let response = try await client?.patch(endpoint: Accounts.updateCredentials(json: data)) if response?.statusCode != 200 { saveError = true @@ -137,12 +138,13 @@ import SwiftUI private func uploadHeader(data: Data) async -> Bool { guard let client else { return false } do { - let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia, - version: .v1, - method: "PATCH", - mimeType: "image/jpeg", - filename: "header", - data: data) + let response = try await client.mediaUpload( + endpoint: Accounts.updateCredentialsMedia, + version: .v1, + method: "PATCH", + mimeType: "image/jpeg", + filename: "header", + data: data) return response?.statusCode == 200 } catch { return false @@ -152,12 +154,13 @@ import SwiftUI private func uploadAvatar(data: Data) async -> Bool { guard let client else { return false } do { - let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia, - version: .v1, - method: "PATCH", - mimeType: "image/jpeg", - filename: "avatar", - data: data) + let response = try await client.mediaUpload( + endpoint: Accounts.updateCredentialsMedia, + version: .v1, + method: "PATCH", + mimeType: "image/jpeg", + filename: "avatar", + data: data) return response?.statusCode == 200 } catch { return false @@ -165,18 +168,21 @@ import SwiftUI } private func getItemImageData(item: PhotosPickerItem, for type: ItemType) async -> Data? { - guard let imageFile = try? await item.loadTransferable(type: StatusEditor.ImageFileTranseferable.self) else { return nil } + guard + let imageFile = try? await item.loadTransferable( + type: StatusEditor.ImageFileTranseferable.self) + else { return nil } let compressor = StatusEditor.Compressor() guard let compressedData = await compressor.compressImageFrom(url: imageFile.url), - let image = UIImage(data: compressedData), - let uploadData = try? await compressor.compressImageForUpload( - image, - maxSize: 2 * 1024 * 1024, // 2MB - maxHeight: type.maxHeight, - maxWidth: type.maxWidth - ) + let image = UIImage(data: compressedData), + let uploadData = try? await compressor.compressImageForUpload( + image, + maxSize: 2 * 1024 * 1024, // 2MB + maxHeight: type.maxHeight, + maxWidth: type.maxWidth + ) else { return nil } diff --git a/Packages/Account/Sources/Account/Edit/EditRelationshipNoteView.swift b/Packages/Account/Sources/Account/Edit/EditRelationshipNoteView.swift index 9c42e258..dd3e5781 100644 --- a/Packages/Account/Sources/Account/Edit/EditRelationshipNoteView.swift +++ b/Packages/Account/Sources/Account/Edit/EditRelationshipNoteView.swift @@ -15,27 +15,31 @@ public struct EditRelationshipNoteView: View { NavigationStack { Form { Section("account.relation.note.label") { - TextField("account.relation.note.edit.placeholder", text: $viewModel.note, axis: .vertical) - .frame(minHeight: 150, maxHeight: 150, alignment: .top) + TextField( + "account.relation.note.edit.placeholder", text: $viewModel.note, axis: .vertical + ) + .frame(minHeight: 150, maxHeight: 150, alignment: .top) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } #if !os(visionOS) - .scrollContentBackground(.hidden) - .background(theme.secondaryBackgroundColor) + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) #endif .navigationTitle("account.relation.note.edit") .navigationBarTitleDisplayMode(.inline) .toolbar { toolbarContent } - .alert("account.relation.note.edit.error.save.title", - isPresented: $viewModel.saveError, - actions: { - Button("alert.button.ok", action: {}) - }, message: { Text("account.relation.note.edit.error.save.message") }) + .alert( + "account.relation.note.edit.error.save.title", + isPresented: $viewModel.saveError, + actions: { + Button("alert.button.ok", action: {}) + }, message: { Text("account.relation.note.edit.error.save.message") } + ) .task { viewModel.client = client viewModel.relatedAccountId = accountDetailViewModel.accountId diff --git a/Packages/Account/Sources/Account/Edit/EditRelationshipNoteViewModel.swift b/Packages/Account/Sources/Account/Edit/EditRelationshipNoteViewModel.swift index 1f0efbf5..26f59cd9 100644 --- a/Packages/Account/Sources/Account/Edit/EditRelationshipNoteViewModel.swift +++ b/Packages/Account/Sources/Account/Edit/EditRelationshipNoteViewModel.swift @@ -15,11 +15,13 @@ import SwiftUI func save() async { if relatedAccountId != nil, - client != nil + client != nil { isSaving = true do { - _ = try await client!.post(endpoint: Accounts.relationshipNote(id: relatedAccountId!, json: RelationshipNoteData(note: note))) + _ = try await client!.post( + endpoint: Accounts.relationshipNote( + id: relatedAccountId!, json: RelationshipNoteData(note: note))) } catch { isSaving = false saveError = true diff --git a/Packages/Account/Sources/Account/Filters/EditFilterView.swift b/Packages/Account/Sources/Account/Filters/EditFilterView.swift index c51e511b..cb7abf2e 100644 --- a/Packages/Account/Sources/Account/Filters/EditFilterView.swift +++ b/Packages/Account/Sources/Account/Filters/EditFilterView.swift @@ -29,19 +29,21 @@ struct EditFilterView: View { @FocusState private var focusedField: Fields? private var data: ServerFilterData { - let expiresIn: String? = switch expirySelection { - case .infinite: - "" // need to send an empty value in order for the server to clear this field in the filter - case .custom: - String(Int(expiresAt?.timeIntervalSince(Date()) ?? 0) + 50) - default: - String(expirySelection.rawValue + 50) - } + let expiresIn: String? = + switch expirySelection { + case .infinite: + "" // need to send an empty value in order for the server to clear this field in the filter + case .custom: + String(Int(expiresAt?.timeIntervalSince(Date()) ?? 0) + 50) + default: + String(expirySelection.rawValue + 50) + } - return ServerFilterData(title: title, - context: contexts, - filterAction: filterAction, - expiresIn: expiresIn) + return ServerFilterData( + title: title, + context: contexts, + filterAction: filterAction, + expiresIn: expiresIn) } private var canSave: Bool { @@ -75,16 +77,16 @@ struct EditFilterView: View { .scrollDismissesKeyboard(.interactively) .background(theme.secondaryBackgroundColor) #endif - .onAppear { - if filter == nil { - focusedField = .title - } + .onAppear { + if filter == nil { + focusedField = .title } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - saveButton - } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + saveButton } + } } private var expirySection: some View { @@ -100,14 +102,16 @@ struct EditFilterView: View { } } if expirySelection != .infinite { - DatePicker("filter.edit.expiry.date-time", - selection: Binding(get: { expiresAt ?? Date() }, set: { expiresAt = $0 }), - displayedComponents: [.date, .hourAndMinute]) - .disabled(expirySelection != .custom) + DatePicker( + "filter.edit.expiry.date-time", + selection: Binding(get: { expiresAt ?? Date() }, set: { expiresAt = $0 }), + displayedComponents: [.date, .hourAndMinute] + ) + .disabled(expirySelection != .custom) } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -123,7 +127,7 @@ struct EditFilterView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif if filter == nil, !title.isEmpty { @@ -145,7 +149,7 @@ struct EditFilterView: View { .transition(.opacity) } #if !os(visionOS) - .listRowBackground(theme.secondaryBackgroundColor) + .listRowBackground(theme.secondaryBackgroundColor) #endif } } @@ -201,31 +205,35 @@ struct EditFilterView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } private var contextsSection: some View { Section("filter.edit.contexts") { ForEach(ServerFilter.Context.allCases, id: \.self) { context in - Toggle(isOn: .init(get: { - contexts.contains(where: { $0 == context }) - }, set: { _ in - if let index = contexts.firstIndex(of: context) { - contexts.remove(at: index) - } else { - contexts.append(context) - } - Task { - await saveFilter(client) - } - })) { + Toggle( + isOn: .init( + get: { + contexts.contains(where: { $0 == context }) + }, + set: { _ in + if let index = contexts.firstIndex(of: context) { + contexts.remove(at: index) + } else { + contexts.append(context) + } + Task { + await saveFilter(client) + } + }) + ) { Label(context.name, systemImage: context.iconName) } .disabled(isSavingFilter) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } } @@ -248,7 +256,7 @@ struct EditFilterView: View { .pickerStyle(.inline) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -277,11 +285,13 @@ struct EditFilterView: View { do { isSavingFilter = true if let filter { - self.filter = try await client.put(endpoint: ServerFilters.editFilter(id: filter.id, json: data), - forceVersion: .v2) + self.filter = try await client.put( + endpoint: ServerFilters.editFilter(id: filter.id, json: data), + forceVersion: .v2) } else { - let newFilter: ServerFilter = try await client.post(endpoint: ServerFilters.createFilter(json: data), - forceVersion: .v2) + let newFilter: ServerFilter = try await client.post( + endpoint: ServerFilters.createFilter(json: data), + forceVersion: .v2) filter = newFilter } } catch {} @@ -292,11 +302,12 @@ struct EditFilterView: View { guard let filterId = filter?.id else { return } isSavingFilter = true do { - let keyword: ServerFilter.Keyword = try await - client.post(endpoint: ServerFilters.addKeyword(filter: filterId, - keyword: name, - wholeWord: true), - forceVersion: .v2) + let keyword: ServerFilter.Keyword = try await client.post( + endpoint: ServerFilters.addKeyword( + filter: filterId, + keyword: name, + wholeWord: true), + forceVersion: .v2) keywords.append(keyword) } catch {} isSavingFilter = false @@ -305,8 +316,9 @@ struct EditFilterView: View { private func deleteKeyword(_ client: Client, keyword: ServerFilter.Keyword) async { isSavingFilter = true do { - let response = try await client.delete(endpoint: ServerFilters.removeKeyword(id: keyword.id), - forceVersion: .v2) + let response = try await client.delete( + endpoint: ServerFilters.removeKeyword(id: keyword.id), + forceVersion: .v2) if response?.statusCode == 200 { keywords.removeAll(where: { $0.id == keyword.id }) } diff --git a/Packages/Account/Sources/Account/Filters/FiltersListView.swift b/Packages/Account/Sources/Account/Filters/FiltersListView.swift index 33b1d296..48d8d5c2 100644 --- a/Packages/Account/Sources/Account/Filters/FiltersListView.swift +++ b/Packages/Account/Sources/Account/Filters/FiltersListView.swift @@ -55,7 +55,7 @@ public struct FiltersListView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } @@ -65,7 +65,7 @@ public struct FiltersListView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .toolbar { @@ -77,15 +77,15 @@ public struct FiltersListView: View { .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) #endif - .task { - do { - isLoading = true - filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2) - isLoading = false - } catch { - isLoading = false - } + .task { + do { + isLoading = true + filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2) + isLoading = false + } catch { + isLoading = false } + } } } @@ -93,8 +93,9 @@ public struct FiltersListView: View { if let index = indexes.first { Task { do { - let response = try await client.delete(endpoint: ServerFilters.filter(id: filters[index].id), - forceVersion: .v2) + let response = try await client.delete( + endpoint: ServerFilters.filter(id: filters[index].id), + forceVersion: .v2) if response?.statusCode == 200 { filters.remove(at: index) } diff --git a/Packages/Account/Sources/Account/Follow/FollowButton.swift b/Packages/Account/Sources/Account/Follow/FollowButton.swift index 37c0aa21..51146cfe 100644 --- a/Packages/Account/Sources/Account/Follow/FollowButton.swift +++ b/Packages/Account/Sources/Account/Follow/FollowButton.swift @@ -3,8 +3,8 @@ import Combine import Foundation import Models import Network -import Observation import OSLog +import Observation import SwiftUI @MainActor @@ -16,12 +16,13 @@ import SwiftUI public let relationshipUpdated: (Relationship) -> Void public var relationship: Relationship - public init(client: Client, - accountId: String, - relationship: Relationship, - shouldDisplayNotify: Bool, - relationshipUpdated: @escaping ((Relationship) -> Void)) - { + public init( + client: Client, + accountId: String, + relationship: Relationship, + shouldDisplayNotify: Bool, + relationshipUpdated: @escaping ((Relationship) -> Void) + ) { self.client = client self.accountId = accountId self.relationship = relationship @@ -31,7 +32,8 @@ import SwiftUI func follow() async throws { do { - relationship = try await client.post(endpoint: Accounts.follow(id: accountId, notify: false, reblogs: true)) + relationship = try await client.post( + endpoint: Accounts.follow(id: accountId, notify: false, reblogs: true)) relationshipUpdated(relationship) } catch { throw error @@ -46,9 +48,10 @@ import SwiftUI throw error } } - + func refreshRelationship() async throws { - let relationships: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [accountId])) + let relationships: [Relationship] = try await client.get( + endpoint: Accounts.relationships(ids: [accountId])) if let relationship = relationships.first { self.relationship = relationship relationshipUpdated(relationship) @@ -57,9 +60,11 @@ import SwiftUI func toggleNotify() async throws { do { - relationship = try await client.post(endpoint: Accounts.follow(id: accountId, - notify: !relationship.notifying, - reblogs: relationship.showingReblogs)) + relationship = try await client.post( + endpoint: Accounts.follow( + id: accountId, + notify: !relationship.notifying, + reblogs: relationship.showingReblogs)) relationshipUpdated(relationship) } catch { throw error @@ -68,9 +73,11 @@ import SwiftUI func toggleReboosts() async throws { do { - relationship = try await client.post(endpoint: Accounts.follow(id: accountId, - notify: relationship.notifying, - reblogs: !relationship.showingReblogs)) + relationship = try await client.post( + endpoint: Accounts.follow( + id: accountId, + notify: relationship.notifying, + reblogs: !relationship.showingReblogs)) relationshipUpdated(relationship) } catch { throw error @@ -98,13 +105,17 @@ public struct FollowButton: View { if viewModel.relationship.requested == true { Text("account.follow.requested") } else { - Text(viewModel.relationship.following ? "account.follow.following" : "account.follow.follow") - .accessibilityLabel("account.follow.following") - .accessibilityValue(viewModel.relationship.following ? "accessibility.general.toggle.on" : "accessibility.general.toggle.off") + Text( + viewModel.relationship.following ? "account.follow.following" : "account.follow.follow" + ) + .accessibilityLabel("account.follow.following") + .accessibilityValue( + viewModel.relationship.following + ? "accessibility.general.toggle.on" : "accessibility.general.toggle.off") } } if viewModel.relationship.following, - viewModel.shouldDisplayNotify + viewModel.shouldDisplayNotify { HStack { AsyncButton { @@ -113,14 +124,18 @@ public struct FollowButton: View { Image(systemName: viewModel.relationship.notifying ? "bell.fill" : "bell") } .accessibilityLabel("accessibility.tabs.profile.user-notifications.label") - .accessibilityValue(viewModel.relationship.notifying ? "accessibility.general.toggle.on" : "accessibility.general.toggle.off") + .accessibilityValue( + viewModel.relationship.notifying + ? "accessibility.general.toggle.on" : "accessibility.general.toggle.off") AsyncButton { try await viewModel.toggleReboosts() } label: { Image(viewModel.relationship.showingReblogs ? "Rocket.Fill" : "Rocket") } .accessibilityLabel("accessibility.tabs.profile.user-reblogs.label") - .accessibilityValue(viewModel.relationship.showingReblogs ? "accessibility.general.toggle.on" : "accessibility.general.toggle.off") + .accessibilityValue( + viewModel.relationship.showingReblogs + ? "accessibility.general.toggle.on" : "accessibility.general.toggle.off") } .asyncButtonStyle(.none) .disabledWhenLoading() diff --git a/Packages/Account/Sources/Account/Lists/ListsListView.swift b/Packages/Account/Sources/Account/Lists/ListsListView.swift index 766081e6..d314b82a 100644 --- a/Packages/Account/Sources/Account/Lists/ListsListView.swift +++ b/Packages/Account/Sources/Account/Lists/ListsListView.swift @@ -17,7 +17,7 @@ public struct ListsListView: View { .font(.scaledHeadline) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } .onDelete { index in @@ -35,8 +35,8 @@ public struct ListsListView: View { await currentAccount.fetchLists() } #if !os(visionOS) - .scrollContentBackground(.hidden) - .background(theme.secondaryBackgroundColor) + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) #endif .listStyle(.plain) .navigationTitle("timeline.filter.lists") diff --git a/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift b/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift index 443d3ef2..a0026708 100644 --- a/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift +++ b/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift @@ -23,11 +23,14 @@ public struct AccountDetailMediaGridView: View { public var body: some View { ScrollView(.vertical) { - LazyVGrid(columns: [.init(.flexible(minimum: 100), spacing: 4), - .init(.flexible(minimum: 100), spacing: 4), - .init(.flexible(minimum: 100), spacing: 4)], - spacing: 4) - { + LazyVGrid( + columns: [ + .init(.flexible(minimum: 100), spacing: 4), + .init(.flexible(minimum: 100), spacing: 4), + .init(.flexible(minimum: 100), spacing: 4), + ], + spacing: 4 + ) { ForEach(mediaStatuses) { status in GeometryReader { proxy in if let url = status.attachment.url { @@ -60,12 +63,14 @@ public struct AccountDetailMediaGridView: View { } .contextMenu { Button { - quickLook.prepareFor(selectedMediaAttachment: status.attachment, - mediaAttachments: status.status.mediaAttachments) + quickLook.prepareFor( + selectedMediaAttachment: status.attachment, + mediaAttachments: status.status.mediaAttachments) } label: { Label("Open Media", systemImage: "photo") } - MediaUIShareLink(url: url, type: status.attachment.supportedType == .image ? .image : .av) + MediaUIShareLink( + url: url, type: status.attachment.supportedType == .image ? .image : .av) Button { Task { let transferable = MediaUIImageTransferable(url: url) @@ -104,13 +109,15 @@ public struct AccountDetailMediaGridView: View { private func fetchNextPage() async throws { let newStatuses: [Status] = - try await client.get(endpoint: Accounts.statuses(id: account.id, - sinceId: mediaStatuses.last?.id, - tag: nil, - onlyMedia: true, - excludeReplies: true, - excludeReblogs: true, - pinned: nil)) + try await client.get( + endpoint: Accounts.statuses( + id: account.id, + sinceId: mediaStatuses.last?.id, + tag: nil, + onlyMedia: true, + excludeReplies: true, + excludeReblogs: true, + pinned: nil)) mediaStatuses.append(contentsOf: newStatuses.flatMap { $0.asMediaStatus }) } } diff --git a/Packages/Account/Sources/Account/StatusesLists/AccountStatusesListView.swift b/Packages/Account/Sources/Account/StatusesLists/AccountStatusesListView.swift index cf89590a..c16b3114 100644 --- a/Packages/Account/Sources/Account/StatusesLists/AccountStatusesListView.swift +++ b/Packages/Account/Sources/Account/StatusesLists/AccountStatusesListView.swift @@ -27,24 +27,24 @@ public struct AccountStatusesListView: View { .scrollContentBackground(.hidden) .background(theme.primaryBackgroundColor) #endif - .navigationTitle(viewModel.mode.title) - .navigationBarTitleDisplayMode(.inline) - .refreshable { - await viewModel.fetchNewestStatuses(pullToRefresh: true) - } - .task { - guard !isLoaded else { return } - viewModel.client = client + .navigationTitle(viewModel.mode.title) + .navigationBarTitleDisplayMode(.inline) + .refreshable { + await viewModel.fetchNewestStatuses(pullToRefresh: true) + } + .task { + guard !isLoaded else { return } + viewModel.client = client + await viewModel.fetchNewestStatuses(pullToRefresh: false) + isLoaded = true + } + .onChange(of: client.id) { _, _ in + isLoaded = false + viewModel.client = client + Task { await viewModel.fetchNewestStatuses(pullToRefresh: false) isLoaded = true } - .onChange(of: client.id) { _, _ in - isLoaded = false - viewModel.client = client - Task { - await viewModel.fetchNewestStatuses(pullToRefresh: false) - isLoaded = true - } - } + } } } diff --git a/Packages/Account/Sources/Account/StatusesLists/AccountStatusesListViewModel.swift b/Packages/Account/Sources/Account/StatusesLists/AccountStatusesListViewModel.swift index c617efd5..0cd7ec28 100644 --- a/Packages/Account/Sources/Account/StatusesLists/AccountStatusesListViewModel.swift +++ b/Packages/Account/Sources/Account/StatusesLists/AccountStatusesListViewModel.swift @@ -46,8 +46,9 @@ public class AccountStatusesListViewModel: StatusesFetcher { do { (statuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nil)) StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client) - statusesState = .display(statuses: statuses, - nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none) + statusesState = .display( + statuses: statuses, + nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none) } catch { statusesState = .error(error: error) } @@ -59,8 +60,9 @@ public class AccountStatusesListViewModel: StatusesFetcher { (newStatuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nextId)) statuses.append(contentsOf: newStatuses) StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client) - statusesState = .display(statuses: statuses, - nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none) + statusesState = .display( + statuses: statuses, + nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none) } public func statusDidAppear(status _: Status) {} diff --git a/Packages/Account/Sources/Account/Tags/FollowedTagsListView.swift b/Packages/Account/Sources/Account/Tags/FollowedTagsListView.swift index ca3a9180..d20b82eb 100644 --- a/Packages/Account/Sources/Account/Tags/FollowedTagsListView.swift +++ b/Packages/Account/Sources/Account/Tags/FollowedTagsListView.swift @@ -12,9 +12,9 @@ public struct FollowedTagsListView: View { public var body: some View { List(currentAccount.tags) { tag in TagRowView(tag: tag) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif .padding(.vertical, 4) } .task { @@ -24,8 +24,8 @@ public struct FollowedTagsListView: View { await currentAccount.fetchFollowedTags() } #if !os(visionOS) - .scrollContentBackground(.hidden) - .background(theme.secondaryBackgroundColor) + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) #endif .listStyle(.plain) .navigationTitle("timeline.filter.tags") diff --git a/Packages/Account/Sources/Account/Tip/PremiumAcccountSubsciptionSheetView.swift b/Packages/Account/Sources/Account/Tip/PremiumAcccountSubsciptionSheetView.swift index 6b63cca3..e9a05659 100644 --- a/Packages/Account/Sources/Account/Tip/PremiumAcccountSubsciptionSheetView.swift +++ b/Packages/Account/Sources/Account/Tip/PremiumAcccountSubsciptionSheetView.swift @@ -1,10 +1,10 @@ -import SwiftUI -import Models -import Env -import DesignSystem -import WrappingHStack import AppAccount +import DesignSystem +import Env +import Models import Network +import SwiftUI +import WrappingHStack @MainActor struct PremiumAcccountSubsciptionSheetView: View { @@ -13,20 +13,20 @@ struct PremiumAcccountSubsciptionSheetView: View { @Environment(\.openURL) private var openURL @Environment(AppAccountsManager.self) private var appAccount: AppAccountsManager @Environment(\.colorScheme) private var colorScheme - + @State private var isSubscibeSelected: Bool = false - + private enum SheetState: Int, Equatable { case selection, preparing, webview } - + @State private var state: SheetState = .selection @State private var animationsending: Bool = false @State private var subClubUser: SubClubUser? - + let account: Account let subClubClient = SubClubClient() - + var body: some View { VStack { switch state { @@ -52,7 +52,7 @@ struct PremiumAcccountSubsciptionSheetView: View { } } } - + @ViewBuilder private var tipView: some View { HStack { @@ -91,9 +91,9 @@ struct PremiumAcccountSubsciptionSheetView: View { .background(theme.secondaryBackgroundColor.opacity(0.4)) .cornerRadius(8) .padding(12) - + Spacer() - + if isSubscibeSelected { Button { withAnimation { @@ -111,7 +111,7 @@ struct PremiumAcccountSubsciptionSheetView: View { .padding(.bottom, 38) } } - + private var preparingView: some View { Label("Preparing...", systemImage: "wifi") .symbolEffect(.variableColor.iterative, options: .repeating, value: animationsending) @@ -129,7 +129,7 @@ struct PremiumAcccountSubsciptionSheetView: View { } } } - + private var webView: some View { VStack(alignment: .center) { Text("Almost there...") @@ -139,9 +139,13 @@ struct PremiumAcccountSubsciptionSheetView: View { .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if let subscription = subClubUser?.subscription, - let accountName = appAccount.currentAccount.accountName, - let premiumUsername = account.premiumUsername, - let url = URL(string: "https://\(AppInfo.premiumInstance)/@\(premiumUsername)/subscribe?callback=icecubesapp://subclub&id=@\(accountName)&amount=\(subscription.unitAmount)¤cy=\(subscription.currency)&theme=\(colorScheme)") { + let accountName = appAccount.currentAccount.accountName, + let premiumUsername = account.premiumUsername, + let url = URL( + string: + "https://\(AppInfo.premiumInstance)/@\(premiumUsername)/subscribe?callback=icecubesapp://subclub&id=@\(accountName)&amount=\(subscription.unitAmount)¤cy=\(subscription.currency)&theme=\(colorScheme)" + ) + { openURL(url) } } diff --git a/Packages/Account/Tests/AccountTests/AccountTests.swift b/Packages/Account/Tests/AccountTests/AccountTests.swift index f16a89fe..f26fa27d 100644 --- a/Packages/Account/Tests/AccountTests/AccountTests.swift +++ b/Packages/Account/Tests/AccountTests/AccountTests.swift @@ -1,6 +1,7 @@ -@testable import Account import XCTest +@testable import Account + final class AccountTests: XCTestCase { func testExample() throws { // This is an example of a functional test case. diff --git a/Packages/AppAccount/Package.swift b/Packages/AppAccount/Package.swift index 26fba487..1cf86cda 100644 --- a/Packages/AppAccount/Package.swift +++ b/Packages/AppAccount/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "AppAccount", targets: ["AppAccount"] - ), + ) ], dependencies: [ .package(name: "Network", path: "../Network"), @@ -32,8 +32,8 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] - ), + ) ] ) diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccount.swift b/Packages/AppAccount/Sources/AppAccount/AppAccount.swift index 6c76a86d..a4efa175 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccount.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccount.swift @@ -4,7 +4,7 @@ import Models import Network import SwiftUI -public extension AppAccount { +extension AppAccount { private static var keychain: KeychainSwift { let keychain = KeychainSwift() #if !DEBUG && !targetEnvironment(simulator) @@ -13,17 +13,17 @@ public extension AppAccount { return keychain } - func save() throws { + public func save() throws { let encoder = JSONEncoder() let data = try encoder.encode(self) Self.keychain.set(data, forKey: key, withAccess: .accessibleAfterFirstUnlock) } - func delete() { + public func delete() { Self.keychain.delete(key) } - static func retrieveAll() -> [AppAccount] { + public static func retrieveAll() -> [AppAccount] { let keychain = Self.keychain let decoder = JSONDecoder() let keys = keychain.allKeys @@ -39,7 +39,7 @@ public extension AppAccount { return accounts } - static func deleteAll() { + public static func deleteAll() { let keychain = Self.keychain let keys = keychain.allKeys for key in keys { diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift index c2639727..0e968dc5 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift @@ -48,10 +48,11 @@ public struct AppAccountView: View { private var fullView: some View { Button { if appAccounts.currentAccount.id == viewModel.appAccount.id, - let account = viewModel.account + let account = viewModel.account { if viewModel.isInSettings { - routerPath.navigate(to: .accountSettingsWithAccount(account: account, appAccount: viewModel.appAccount)) + routerPath.navigate( + to: .accountSettingsWithAccount(account: account, appAccount: viewModel.appAccount)) HapticManager.shared.fireHaptic(.buttonPress) } else { isParentPresented = false @@ -76,9 +77,9 @@ public struct AppAccountView: View { .foregroundStyle(.white, .green) .offset(x: 5, y: -5) } else if viewModel.showBadge, - let token = viewModel.appAccount.oauthToken, - let notificationsCount = preferences.notificationsCount[token], - notificationsCount > 0 + let token = viewModel.appAccount.oauthToken, + let notificationsCount = preferences.notificationsCount[token], + notificationsCount > 0 { ZStack { Circle() diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift index 387fd4d5..a5823b48 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift @@ -32,7 +32,10 @@ import SwiftUI } } - public init(appAccount: AppAccount, isCompact: Bool = false, isInSettings: Bool = true, showBadge: Bool = false) { + public init( + appAccount: AppAccount, isCompact: Bool = false, isInSettings: Bool = true, + showBadge: Bool = false + ) { self.appAccount = appAccount self.isCompact = isCompact self.isInSettings = isInSettings diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift index d0094448..7f66379c 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift @@ -13,8 +13,9 @@ import SwiftUI public var currentAccount: AppAccount { didSet { Self.latestCurrentAccountKey = currentAccount.id - currentClient = .init(server: currentAccount.server, - oauthToken: currentAccount.oauthToken) + currentClient = .init( + server: currentAccount.server, + oauthToken: currentAccount.oauthToken) } } @@ -29,10 +30,12 @@ import SwiftUI public static var shared = AppAccountsManager() init() { - var defaultAccount = AppAccount(server: AppInfo.defaultServer, accountName: nil, oauthToken: nil) + var defaultAccount = AppAccount( + server: AppInfo.defaultServer, accountName: nil, oauthToken: nil) let keychainAccounts = AppAccount.retrieveAll() availableAccounts = keychainAccounts - if let currentAccount = keychainAccounts.first(where: { $0.id == Self.latestCurrentAccountKey }) { + if let currentAccount = keychainAccounts.first(where: { $0.id == Self.latestCurrentAccountKey }) + { defaultAccount = currentAccount } else { defaultAccount = keychainAccounts.last ?? defaultAccount @@ -53,9 +56,12 @@ import SwiftUI availableAccounts.removeAll(where: { $0.id == account.id }) account.delete() if currentAccount.id == account.id { - currentAccount = availableAccounts.first ?? AppAccount(server: AppInfo.defaultServer, - accountName: nil, - oauthToken: nil) + currentAccount = + availableAccounts.first + ?? AppAccount( + server: AppInfo.defaultServer, + accountName: nil, + oauthToken: nil) } } } diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift index 2f6ef423..e41b2352 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift @@ -31,10 +31,11 @@ public struct AppAccountsSelectorView: View { return baseHeight } - public init(routerPath: RouterPath, - accountCreationEnabled: Bool = true, - avatarConfig: AvatarView.FrameConfig? = nil) - { + public init( + routerPath: RouterPath, + accountCreationEnabled: Bool = true, + avatarConfig: AvatarView.FrameConfig? = nil + ) { self.routerPath = routerPath self.accountCreationEnabled = accountCreationEnabled self.avatarConfig = avatarConfig ?? .badge @@ -48,14 +49,17 @@ public struct AppAccountsSelectorView: View { labelView .contentShape(Rectangle()) } - .sheet(isPresented: $isPresented, content: { - accountsView.presentationDetents([.height(preferredHeight), .large]) - .presentationBackground(.ultraThinMaterial) - .presentationCornerRadius(16) - .onAppear { - refreshAccounts() - } - }) + .sheet( + isPresented: $isPresented, + content: { + accountsView.presentationDetents([.height(preferredHeight), .large]) + .presentationBackground(.ultraThinMaterial) + .presentationCornerRadius(16) + .onAppear { + refreshAccounts() + } + } + ) .onChange(of: currentAccount.account?.id) { refreshAccounts() } @@ -92,16 +96,17 @@ public struct AppAccountsSelectorView: View { NavigationStack { List { Section { - ForEach(accountsViewModel.sorted { $0.acct < $1.acct }, id: \.appAccount.id) { viewModel in + ForEach(accountsViewModel.sorted { $0.acct < $1.acct }, id: \.appAccount.id) { + viewModel in AppAccountView(viewModel: viewModel, isParentPresented: $isPresented) } addAccountButton - #if os(visionOS) - .foregroundStyle(theme.labelColor) - #endif + #if os(visionOS) + .foregroundStyle(theme.labelColor) + #endif } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor.opacity(0.4)) + .listRowBackground(theme.primaryBackgroundColor.opacity(0.4)) #endif if accountCreationEnabled { @@ -111,9 +116,9 @@ public struct AppAccountsSelectorView: View { supportButton } #if os(visionOS) - .foregroundStyle(theme.labelColor) + .foregroundStyle(theme.labelColor) #else - .listRowBackground(theme.primaryBackgroundColor.opacity(0.4)) + .listRowBackground(theme.primaryBackgroundColor.opacity(0.4)) #endif } } @@ -186,7 +191,8 @@ public struct AppAccountsSelectorView: View { private func refreshAccounts() { accountsViewModel = [] for account in appAccounts.availableAccounts { - let viewModel: AppAccountViewModel = .init(appAccount: account, isInSettings: false, showBadge: true) + let viewModel: AppAccountViewModel = .init( + appAccount: account, isInSettings: false, showBadge: true) accountsViewModel.append(viewModel) } } diff --git a/Packages/Conversations/Package.swift b/Packages/Conversations/Package.swift index 5c92622d..054523c2 100644 --- a/Packages/Conversations/Package.swift +++ b/Packages/Conversations/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "Conversations", targets: ["Conversations"] - ), + ) ], dependencies: [ .package(name: "Models", path: "../Models"), @@ -32,8 +32,8 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] - ), + ) ] ) diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift index cf345a72..8658fda4 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift @@ -37,17 +37,19 @@ public struct ConversationDetailView: View { loadingView } ForEach(viewModel.messages) { message in - ConversationMessageView(message: message, - conversation: viewModel.conversation) - .padding(.vertical, 4) - .id(message.id) + ConversationMessageView( + message: message, + conversation: viewModel.conversation + ) + .padding(.vertical, 4) + .id(message.id) } bottomAnchorView } .padding(.horizontal, .layoutPadding) } #if !os(visionOS) - .scrollDismissesKeyboard(.interactively) + .scrollDismissesKeyboard(.interactively) #endif .safeAreaInset(edge: .bottom) { inputTextView @@ -74,32 +76,32 @@ public struct ConversationDetailView: View { .scrollContentBackground(.hidden) .background(theme.primaryBackgroundColor) #endif - .toolbar { - ToolbarItem(placement: .principal) { - if viewModel.conversation.accounts.count == 1, - let account = viewModel.conversation.accounts.first - { - EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis) - .font(.scaledHeadline) - .foregroundColor(theme.labelColor) - .emojiText.size(Font.scaledHeadlineFont.emojiSize) - .emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset) - } else { - Text("Direct message with \(viewModel.conversation.accounts.count) people") - .font(.scaledHeadline) - } - } - } - .onChange(of: watcher.latestEvent?.id) { - if let latestEvent = watcher.latestEvent { - viewModel.handleEvent(event: latestEvent) - DispatchQueue.main.async { - withAnimation { - scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom) - } + .toolbar { + ToolbarItem(placement: .principal) { + if viewModel.conversation.accounts.count == 1, + let account = viewModel.conversation.accounts.first + { + EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis) + .font(.scaledHeadline) + .foregroundColor(theme.labelColor) + .emojiText.size(Font.scaledHeadlineFont.emojiSize) + .emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset) + } else { + Text("Direct message with \(viewModel.conversation.accounts.count) people") + .font(.scaledHeadline) + } + } + } + .onChange(of: watcher.latestEvent?.id) { + if let latestEvent = watcher.latestEvent { + viewModel.handleEvent(event: latestEvent) + DispatchQueue.main.async { + withAnimation { + scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom) } } } + } } private var loadingView: some View { @@ -124,23 +126,26 @@ public struct ConversationDetailView: View { HStack(alignment: .bottom, spacing: 8) { if viewModel.conversation.lastStatus != nil { Button { - routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.conversation.lastStatus!) + routerPath.presentedSheet = .replyToStatusEditor( + status: viewModel.conversation.lastStatus!) } label: { Image(systemName: "plus") } .padding(.bottom, 7) } - TextField("conversations.new.message.placeholder", text: $viewModel.newMessageText, axis: .vertical) - .focused($isMessageFieldFocused) - .keyboardType(.default) - .backgroundStyle(.thickMaterial) - .padding(6) - .overlay( - RoundedRectangle(cornerRadius: 14) - .stroke(.gray, lineWidth: 1) - ) - .font(.scaledBody) + TextField( + "conversations.new.message.placeholder", text: $viewModel.newMessageText, axis: .vertical + ) + .focused($isMessageFieldFocused) + .keyboardType(.default) + .backgroundStyle(.thickMaterial) + .padding(6) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(.gray, lineWidth: 1) + ) + .font(.scaledBody) if !viewModel.newMessageText.isEmpty { Button { Task { diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift index 3c6c4ab5..e3a3797e 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift @@ -23,7 +23,8 @@ import SwiftUI func fetchMessages() async { guard let client, let lastMessageId = messages.last?.id else { return } do { - let context: StatusContext = try await client.get(endpoint: Statuses.context(id: lastMessageId)) + let context: StatusContext = try await client.get( + endpoint: Statuses.context(id: lastMessageId)) isLoadingMessages = false messages.insert(contentsOf: context.ancestors, at: 0) messages.append(contentsOf: context.descendants) @@ -36,9 +37,10 @@ import SwiftUI var finalText = conversation.accounts.map { "@\($0.acct)" }.joined(separator: " ") finalText += " " finalText += newMessageText - let data = StatusData(status: finalText, - visibility: .direct, - inReplyToId: messages.last?.id) + let data = StatusData( + status: finalText, + visibility: .direct, + inReplyToId: messages.last?.id) do { let status: Status = try await client.post(endpoint: Statuses.postStatus(json: data)) appendNewStatus(status: status) @@ -53,15 +55,15 @@ import SwiftUI func handleEvent(event: any StreamEvent) { if let event = event as? StreamEventStatusUpdate, - let index = messages.firstIndex(where: { $0.id == event.status.id }) + let index = messages.firstIndex(where: { $0.id == event.status.id }) { messages[index] = event.status } else if let event = event as? StreamEventDelete, - let index = messages.firstIndex(where: { $0.id == event.status }) + let index = messages.firstIndex(where: { $0.id == event.status }) { messages.remove(at: index) } else if let event = event as? StreamEventConversation, - event.conversation.id == conversation.id + event.conversation.id == conversation.id { conversation = event.conversation if conversation.lastStatus != nil { diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift index 61ae9387..7d0290cb 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift @@ -39,14 +39,16 @@ struct ConversationMessageView: View { .emojiText.size(Font.scaledBodyFont.emojiSize) .emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset) .padding(6) - .environment(\.openURL, OpenURLAction { url in - routerPath.handleStatus(status: message, url: url) - }) + .environment( + \.openURL, + OpenURLAction { url in + routerPath.handleStatus(status: message, url: url) + }) } #if os(visionOS) - .background(isOwnMessage ? Material.ultraThick : Material.regular) + .background(isOwnMessage ? Material.ultraThick : Material.regular) #else - .background(isOwnMessage ? theme.tintColor.opacity(0.2) : theme.secondaryBackgroundColor) + .background(isOwnMessage ? theme.tintColor.opacity(0.2) : theme.secondaryBackgroundColor) #endif .cornerRadius(8) .padding(.leading, isOwnMessage ? 24 : 0) @@ -77,8 +79,7 @@ struct ConversationMessageView: View { Spacer() } Group { - Text(message.createdAt.shortDateFormatted) + - Text(" ") + Text(message.createdAt.shortDateFormatted) + Text(" ") Text(message.createdAt.asDate, style: .time) } .font(.scaledFootnote) @@ -122,25 +123,28 @@ struct ConversationMessageView: View { } catch {} } } label: { - Label(isLiked ? "status.action.unfavorite" : "status.action.favorite", - systemImage: isLiked ? "star.fill" : "star") + Label( + isLiked ? "status.action.unfavorite" : "status.action.favorite", + systemImage: isLiked ? "star.fill" : "star") } Button { Task { - do { - let status: Status - if isBookmarked { - status = try await client.post(endpoint: Statuses.unbookmark(id: message.id)) - } else { - status = try await client.post(endpoint: Statuses.bookmark(id: message.id)) - } - withAnimation { - isBookmarked = status.bookmarked == true - } - } catch {} - } } label: { - Label(isBookmarked ? "status.action.unbookmark" : "status.action.bookmark", - systemImage: isBookmarked ? "bookmark.fill" : "bookmark") + do { + let status: Status + if isBookmarked { + status = try await client.post(endpoint: Statuses.unbookmark(id: message.id)) + } else { + status = try await client.post(endpoint: Statuses.bookmark(id: message.id)) + } + withAnimation { + isBookmarked = status.bookmarked == true + } + } catch {} + } + } label: { + Label( + isBookmarked ? "status.action.unbookmark" : "status.action.bookmark", + systemImage: isBookmarked ? "bookmark.fill" : "bookmark") } Divider() if message.account.id == currentAccount.account?.id { @@ -152,7 +156,8 @@ struct ConversationMessageView: View { } else { Section(message.reblog?.account.acct ?? message.account.acct) { Button { - routerPath.presentedSheet = .mentionStatusEditor(account: message.reblog?.account ?? message.account, visibility: .pub) + routerPath.presentedSheet = .mentionStatusEditor( + account: message.reblog?.account ?? message.account, visibility: .pub) } label: { Label("status.action.mention", systemImage: "at") } @@ -183,9 +188,11 @@ struct ConversationMessageView: View { GeometryReader { proxy in let width = mediaWidth(proxy: proxy) if let url = attachement.url { - LazyImage(request: makeImageRequest(for: url, - size: .init(width: width, height: 200))) - { state in + LazyImage( + request: makeImageRequest( + for: url, + size: .init(width: width, height: 200)) + ) { state in if let image = state.image { image .resizable() @@ -207,8 +214,10 @@ struct ConversationMessageView: View { .contentShape(Rectangle()) .onTapGesture { #if targetEnvironment(macCatalyst) || os(visionOS) - openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement], - selectedAttachment: attachement)) + openWindow( + value: WindowDestinationMedia.mediaViewer( + attachments: [attachement], + selectedAttachment: attachement)) #else quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement]) #endif diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift index f546a1b4..6c9e3322 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift @@ -29,15 +29,18 @@ struct ConversationsListRow: View { .accessibilityHidden(true) VStack(alignment: .leading, spacing: 4) { HStack { - EmojiTextApp(.init(stringValue: conversation.accounts.map(\.safeDisplayName).joined(separator: ", ")), - emojis: conversation.accounts.flatMap(\.emojis)) - .font(.scaledSubheadline) - .foregroundColor(theme.labelColor) - .emojiText.size(Font.scaledSubheadlineFont.emojiSize) - .emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset) - .fontWeight(.semibold) - .foregroundColor(theme.labelColor) - .multilineTextAlignment(.leading) + EmojiTextApp( + .init( + stringValue: conversation.accounts.map(\.safeDisplayName).joined(separator: ", ")), + emojis: conversation.accounts.flatMap(\.emojis) + ) + .font(.scaledSubheadline) + .foregroundColor(theme.labelColor) + .emojiText.size(Font.scaledSubheadlineFont.emojiSize) + .emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset) + .fontWeight(.semibold) + .foregroundColor(theme.labelColor) + .multilineTextAlignment(.leading) Spacer() if conversation.unread { Circle() @@ -53,13 +56,16 @@ struct ConversationsListRow: View { .font(.scaledFootnote) } } - EmojiTextApp(conversation.lastStatus?.content ?? HTMLString(stringValue: ""), emojis: conversation.lastStatus?.emojis ?? []) - .multilineTextAlignment(.leading) - .font(.scaledBody) - .foregroundColor(theme.labelColor) - .emojiText.size(Font.scaledBodyFont.emojiSize) - .emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset) - .accessibilityLabel(conversation.lastStatus?.content.asRawText ?? "") + EmojiTextApp( + conversation.lastStatus?.content ?? HTMLString(stringValue: ""), + emojis: conversation.lastStatus?.emojis ?? [] + ) + .multilineTextAlignment(.leading) + .font(.scaledBody) + .foregroundColor(theme.labelColor) + .emojiText.size(Font.scaledBodyFont.emojiSize) + .emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset) + .accessibilityLabel(conversation.lastStatus?.content.asRawText ?? "") } Spacer() } @@ -146,7 +152,8 @@ struct ConversationsListRow: View { if message.account.id != currentAccount.account?.id { Section(message.reblog?.account.acct ?? message.account.acct) { Button { - routerPath.presentedSheet = .mentionStatusEditor(account: message.reblog?.account ?? message.account, visibility: .pub) + routerPath.presentedSheet = .mentionStatusEditor( + account: message.reblog?.account ?? message.account, visibility: .pub) } label: { Label("status.action.mention", systemImage: "at") } @@ -177,16 +184,20 @@ struct ConversationsListRow: View { await viewModel.favorite(conversation: conversation) } } label: { - Label(conversation.lastStatus?.favourited ?? false ? "status.action.unfavorite" : "status.action.favorite", - systemImage: conversation.lastStatus?.favourited ?? false ? "star.fill" : "star") + Label( + conversation.lastStatus?.favourited ?? false + ? "status.action.unfavorite" : "status.action.favorite", + systemImage: conversation.lastStatus?.favourited ?? false ? "star.fill" : "star") } Button { Task { await viewModel.bookmark(conversation: conversation) } } label: { - Label(conversation.lastStatus?.bookmarked ?? false ? "status.action.unbookmark" : "status.action.bookmark", - systemImage: conversation.lastStatus?.bookmarked ?? false ? "bookmark.fill" : "bookmark") + Label( + conversation.lastStatus?.bookmarked ?? false + ? "status.action.unbookmark" : "status.action.bookmark", + systemImage: conversation.lastStatus?.bookmarked ?? false ? "bookmark.fill" : "bookmark") } } diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift index 4bd49c97..c6cc41c1 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift @@ -14,7 +14,7 @@ public struct ConversationsListView: View { @State private var viewModel = ConversationsListViewModel() - public init() { } + public init() {} private var conversations: Binding<[Conversation]> { if viewModel.isLoadingFirstPage { @@ -44,14 +44,16 @@ public struct ConversationsListView: View { Divider() } } else if conversations.isEmpty, !viewModel.isLoadingFirstPage, !viewModel.isError { - PlaceholderView(iconName: "tray", - title: "conversations.empty.title", - message: "conversations.empty.message") + PlaceholderView( + iconName: "tray", + title: "conversations.empty.title", + message: "conversations.empty.message") } else if viewModel.isError { - ErrorView(title: "conversations.error.title", - message: "conversations.error.message", - buttonTitle: "conversations.error.button") - { + ErrorView( + title: "conversations.error.title", + message: "conversations.error.message", + buttonTitle: "conversations.error.button" + ) { await viewModel.fetchConversations() } } @@ -75,8 +77,8 @@ public struct ConversationsListView: View { .padding(.top, .layoutPadding) } #if !os(visionOS) - .scrollContentBackground(.hidden) - .background(theme.primaryBackgroundColor) + .scrollContentBackground(.hidden) + .background(theme.primaryBackgroundColor) #endif .navigationTitle("conversations.navigation-title") .navigationBarTitleDisplayMode(.inline) diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift index cfdc0e0f..203b34d0 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift @@ -23,7 +23,8 @@ import SwiftUI isLoadingFirstPage = true } do { - (conversations, nextPage) = try await client.getWithLink(endpoint: Conversations.conversations(maxId: nil)) + (conversations, nextPage) = try await client.getWithLink( + endpoint: Conversations.conversations(maxId: nil)) if nextPage?.maxId == nil { nextPage = nil } @@ -39,7 +40,8 @@ import SwiftUI do { isLoadingNextPage = true var nextMessages: [Conversation] = [] - (nextMessages, nextPage) = try await client.getWithLink(endpoint: Conversations.conversations(maxId: maxId)) + (nextMessages, nextPage) = try await client.getWithLink( + endpoint: Conversations.conversations(maxId: maxId)) conversations.append(contentsOf: nextMessages) if nextPage?.maxId == nil { nextPage = nil @@ -62,11 +64,12 @@ import SwiftUI func favorite(conversation: Conversation) async { guard let client, let message = conversation.lastStatus else { return } - let endpoint: Endpoint = if message.favourited ?? false { - Statuses.unfavorite(id: message.id) - } else { - Statuses.favorite(id: message.id) - } + let endpoint: Endpoint = + if message.favourited ?? false { + Statuses.unfavorite(id: message.id) + } else { + Statuses.favorite(id: message.id) + } do { let status: Status = try await client.post(endpoint: endpoint) updateConversationWithNewLastStatus(conversation: conversation, newLastStatus: status) @@ -75,19 +78,24 @@ import SwiftUI func bookmark(conversation: Conversation) async { guard let client, let message = conversation.lastStatus else { return } - let endpoint: Endpoint = if message.bookmarked ?? false { - Statuses.unbookmark(id: message.id) - } else { - Statuses.bookmark(id: message.id) - } + let endpoint: Endpoint = + if message.bookmarked ?? false { + Statuses.unbookmark(id: message.id) + } else { + Statuses.bookmark(id: message.id) + } do { let status: Status = try await client.post(endpoint: endpoint) updateConversationWithNewLastStatus(conversation: conversation, newLastStatus: status) } catch {} } - private func updateConversationWithNewLastStatus(conversation: Conversation, newLastStatus: Status) { - let newConversation = Conversation(id: conversation.id, unread: conversation.unread, lastStatus: newLastStatus, accounts: conversation.accounts) + private func updateConversationWithNewLastStatus( + conversation: Conversation, newLastStatus: Status + ) { + let newConversation = Conversation( + id: conversation.id, unread: conversation.unread, lastStatus: newLastStatus, + accounts: conversation.accounts) updateConversations(conversation: newConversation) } @@ -96,7 +104,9 @@ import SwiftUI conversations.remove(at: index) } conversations.insert(conversation, at: 0) - conversations = conversations.sorted(by: { ($0.lastStatus?.createdAt.asDate ?? Date.now) > ($1.lastStatus?.createdAt.asDate ?? Date.now) }) + conversations = conversations.sorted(by: { + ($0.lastStatus?.createdAt.asDate ?? Date.now) > ($1.lastStatus?.createdAt.asDate ?? Date.now) + }) } func handleEvent(event: any StreamEvent) { diff --git a/Packages/DesignSystem/Package.swift b/Packages/DesignSystem/Package.swift index fed10cea..8f56ca9a 100644 --- a/Packages/DesignSystem/Package.swift +++ b/Packages/DesignSystem/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "DesignSystem", targets: ["DesignSystem"] - ), + ) ], dependencies: [ .package(name: "Models", path: "../Models"), @@ -33,8 +33,8 @@ let package = Package( .product(name: "EmojiText", package: "EmojiText"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] - ), + ) ] ) diff --git a/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift b/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift index 8faa0ebf..e6bccc81 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift @@ -3,20 +3,20 @@ import Models import NukeUI import SwiftUI -public extension Account { +extension Account { private struct Part: Identifiable { let id = UUID().uuidString let value: Substring } - var safeDisplayName: String { + public var safeDisplayName: String { if let displayName, !displayName.isEmpty { return displayName } return "@\(username)" } - var displayNameWithoutEmojis: String { + public var displayNameWithoutEmojis: String { var name = safeDisplayName for emoji in emojis { name = name.replacingOccurrences(of: ":\(emoji.shortcode):", with: "") diff --git a/Packages/DesignSystem/Sources/DesignSystem/ColorSet.swift b/Packages/DesignSystem/Sources/DesignSystem/ColorSet.swift index 37b34f96..65299f88 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/ColorSet.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/ColorSet.swift @@ -1,13 +1,15 @@ import SwiftUI public let availableColorsSets: [ColorSetCouple] = - [.init(light: IceCubeLight(), dark: IceCubeDark()), - .init(light: IceCubeNeonLight(), dark: IceCubeNeonDark()), - .init(light: DesertLight(), dark: DesertDark()), - .init(light: NemesisLight(), dark: NemesisDark()), - .init(light: MediumLight(), dark: MediumDark()), - .init(light: ConstellationLight(), dark: ConstellationDark()), - .init(light: ThreadsLight(), dark: ThreadsDark())] + [ + .init(light: IceCubeLight(), dark: IceCubeDark()), + .init(light: IceCubeNeonLight(), dark: IceCubeNeonDark()), + .init(light: DesertLight(), dark: DesertDark()), + .init(light: NemesisLight(), dark: NemesisDark()), + .init(light: MediumLight(), dark: MediumDark()), + .init(light: ConstellationLight(), dark: ConstellationDark()), + .init(light: ThreadsLight(), dark: ThreadsDark()), + ] public protocol ColorSet: Sendable { var name: ColorSetName { get } diff --git a/Packages/DesignSystem/Sources/DesignSystem/ConditionalModifier.swift b/Packages/DesignSystem/Sources/DesignSystem/ConditionalModifier.swift index 98d56213..1be79413 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/ConditionalModifier.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/ConditionalModifier.swift @@ -1,7 +1,9 @@ import SwiftUI -public extension View { - @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { +extension View { + @ViewBuilder public func `if`(_ condition: Bool, transform: (Self) -> Content) + -> some View + { if condition { transform(self) } else { diff --git a/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift b/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift index ce06be8a..c125d572 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift @@ -1,17 +1,17 @@ import Foundation @MainActor -public extension CGFloat { - static var layoutPadding: CGFloat { +extension CGFloat { + public static var layoutPadding: CGFloat { Theme.shared.compactLayoutPadding ? 20 : 8 } - - static let dividerPadding: CGFloat = 2 - static let scrollToViewHeight: CGFloat = 1 - static let statusColumnsSpacing: CGFloat = 8 - static let statusComponentSpacing: CGFloat = 6 - static let secondaryColumnWidth: CGFloat = 400 - static let sidebarWidth: CGFloat = 90 - static let sidebarWidthExpanded: CGFloat = 220 - static let pollBarHeight: CGFloat = 30 + + public static let dividerPadding: CGFloat = 2 + public static let scrollToViewHeight: CGFloat = 1 + public static let statusColumnsSpacing: CGFloat = 8 + public static let statusComponentSpacing: CGFloat = 6 + public static let secondaryColumnWidth: CGFloat = 400 + public static let sidebarWidth: CGFloat = 90 + public static let sidebarWidthExpanded: CGFloat = 220 + public static let pollBarHeight: CGFloat = 30 } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Font.swift b/Packages/DesignSystem/Sources/DesignSystem/Font.swift index 950b4b0c..e7f99725 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Font.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Font.swift @@ -2,7 +2,7 @@ import Env import SwiftUI @MainActor -public extension Font { +extension Font { // See https://gist.github.com/zacwest/916d31da5d03405809c4 for iOS values // Custom values for Mac private static let title = 28.0 @@ -45,84 +45,85 @@ public extension Font { UIFontMetrics.default.scaledValue(for: baseSize * Theme.shared.fontSizeScale) } - static var scaledTitle: Font { + public static var scaledTitle: Font { customFont(size: userScaledFontSize(baseSize: title), relativeTo: .title) } - static var scaledHeadline: Font { - customFont(size: userScaledFontSize(baseSize: headline), relativeTo: .headline).weight(.semibold) + public static var scaledHeadline: Font { + customFont(size: userScaledFontSize(baseSize: headline), relativeTo: .headline).weight( + .semibold) } - static var scaledHeadlineFont: UIFont { + public static var scaledHeadlineFont: UIFont { customUIFont(size: userScaledFontSize(baseSize: headline)) } - static var scaledBodyFocused: Font { + public static var scaledBodyFocused: Font { customFont(size: userScaledFontSize(baseSize: body + 2), relativeTo: .body) } - static var scaledBodyFocusedFont: UIFont { + public static var scaledBodyFocusedFont: UIFont { customUIFont(size: userScaledFontSize(baseSize: body + 2)) } - static var scaledBody: Font { + public static var scaledBody: Font { customFont(size: userScaledFontSize(baseSize: body), relativeTo: .body) } - static var scaledBodyFont: UIFont { + public static var scaledBodyFont: UIFont { customUIFont(size: userScaledFontSize(baseSize: body)) } - static var scaledBodyUIFont: UIFont { + public static var scaledBodyUIFont: UIFont { customUIFont(size: userScaledFontSize(baseSize: body)) } - static var scaledCallout: Font { + public static var scaledCallout: Font { customFont(size: userScaledFontSize(baseSize: callout), relativeTo: .callout) } - static var scaledCalloutFont: UIFont { + public static var scaledCalloutFont: UIFont { customUIFont(size: userScaledFontSize(baseSize: body)) } - static var scaledSubheadline: Font { + public static var scaledSubheadline: Font { customFont(size: userScaledFontSize(baseSize: subheadline), relativeTo: .subheadline) } - static var scaledSubheadlineFont: UIFont { + public static var scaledSubheadlineFont: UIFont { customUIFont(size: userScaledFontSize(baseSize: subheadline)) } - static var scaledFootnote: Font { + public static var scaledFootnote: Font { customFont(size: userScaledFontSize(baseSize: footnote), relativeTo: .footnote) } - static var scaledFootnoteFont: UIFont { + public static var scaledFootnoteFont: UIFont { customUIFont(size: userScaledFontSize(baseSize: footnote)) } - static var scaledCaption: Font { + public static var scaledCaption: Font { customFont(size: userScaledFontSize(baseSize: caption), relativeTo: .caption) } - static var scaledCaptionFont: UIFont { + public static var scaledCaptionFont: UIFont { customUIFont(size: userScaledFontSize(baseSize: caption)) } } -public extension UIFont { - func rounded() -> UIFont { +extension UIFont { + public func rounded() -> UIFont { guard let descriptor = fontDescriptor.withDesign(.rounded) else { return self } return UIFont(descriptor: descriptor, size: pointSize) } - var emojiSize: CGFloat { + public var emojiSize: CGFloat { pointSize } - var emojiBaselineOffset: CGFloat { + public var emojiBaselineOffset: CGFloat { // Center emoji with capital letter size of font -(emojiSize - capHeight) / 2 } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift b/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift index 409f5caa..eac48dc6 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift @@ -1,19 +1,19 @@ import SwiftUI -public extension Color { - static var brand: Color { +extension Color { + public static var brand: Color { Color(red: 187 / 255, green: 59 / 255, blue: 226 / 255) } - static var primaryBackground: Color { + public static var primaryBackground: Color { Color(red: 16 / 255, green: 21 / 255, blue: 35 / 255) } - static var secondaryBackground: Color { + public static var secondaryBackground: Color { Color(red: 30 / 255, green: 35 / 255, blue: 62 / 255) } - static var label: Color { + public static var label: Color { Color(.label) } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/SFSymbols.swift b/Packages/DesignSystem/Sources/DesignSystem/SFSymbols.swift index bce86087..cf9f4747 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/SFSymbols.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/SFSymbols.swift @@ -1,5 +1,3 @@ - - import Foundation import SwiftUI @@ -7,8 +5,8 @@ import SwiftUI // images named in lower case are Apple's symbols // images inamed in CamelCase are custom -public extension Label where Title == Text, Icon == Image { - init(_ title: LocalizedStringKey, imageNamed: String) { +extension Label where Title == Text, Icon == Image { + public init(_ title: LocalizedStringKey, imageNamed: String) { if imageNamed.lowercased() == imageNamed { self.init(title, systemImage: imageNamed) } else { diff --git a/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift b/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift index f67980e3..a76d9d44 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift @@ -12,10 +12,11 @@ import UIKit public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height #endif - public func scene(_ scene: UIScene, - willConnectTo _: UISceneSession, - options _: UIScene.ConnectionOptions) - { + public func scene( + _ scene: UIScene, + willConnectTo _: UISceneSession, + options _: UIScene.ConnectionOptions + ) { guard let windowScene = scene as? UIWindowScene else { return } window = windowScene.keyWindow @@ -29,12 +30,12 @@ import UIKit override public init() { super.init() - + Task { @MainActor in setup() } } - + private func setup() { #if os(visionOS) windowWidth = window?.bounds.size.width ?? 0 @@ -44,7 +45,7 @@ import UIKit windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height #endif Self.observedSceneDelegate.insert(self) - _ = Self.observer // just for activating the lazy static property + _ = Self.observer // just for activating the lazy static property } private static var observedSceneDelegate: Set = [] diff --git a/Packages/DesignSystem/Sources/DesignSystem/Theme.swift b/Packages/DesignSystem/Sources/DesignSystem/Theme.swift index 35847a22..baeb3f60 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Theme.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Theme.swift @@ -20,18 +20,25 @@ public final class Theme { @AppStorage("is_previously_set") public var isThemePreviouslySet: Bool = false @AppStorage(ThemeKey.selectedScheme.rawValue) public var selectedScheme: ColorScheme = .dark @AppStorage(ThemeKey.tint.rawValue) public var tintColor: Color = .black - @AppStorage(ThemeKey.primaryBackground.rawValue) public var primaryBackgroundColor: Color = .white - @AppStorage(ThemeKey.secondaryBackground.rawValue) public var secondaryBackgroundColor: Color = .gray + @AppStorage(ThemeKey.primaryBackground.rawValue) public var primaryBackgroundColor: Color = + .white + @AppStorage(ThemeKey.secondaryBackground.rawValue) public var secondaryBackgroundColor: Color = + .gray @AppStorage(ThemeKey.label.rawValue) public var labelColor: Color = .black @AppStorage(ThemeKey.avatarPosition2.rawValue) var avatarPosition: AvatarPosition = .leading @AppStorage(ThemeKey.avatarShape2.rawValue) var avatarShape: AvatarShape = .circle @AppStorage(ThemeKey.selectedSet.rawValue) var storedSet: ColorSetName = .iceCubeDark - @AppStorage(ThemeKey.statusActionsDisplay.rawValue) public var statusActionsDisplay: StatusActionsDisplay = .full - @AppStorage(ThemeKey.statusDisplayStyle.rawValue) public var statusDisplayStyle: StatusDisplayStyle = .large - @AppStorage(ThemeKey.followSystemColorSchme.rawValue) public var followSystemColorScheme: Bool = true - @AppStorage(ThemeKey.displayFullUsernameTimeline.rawValue) public var displayFullUsername: Bool = false + @AppStorage(ThemeKey.statusActionsDisplay.rawValue) public var statusActionsDisplay: + StatusActionsDisplay = .full + @AppStorage(ThemeKey.statusDisplayStyle.rawValue) public var statusDisplayStyle: + StatusDisplayStyle = .large + @AppStorage(ThemeKey.followSystemColorSchme.rawValue) public var followSystemColorScheme: Bool = + true + @AppStorage(ThemeKey.displayFullUsernameTimeline.rawValue) public var displayFullUsername: + Bool = false @AppStorage(ThemeKey.lineSpacing.rawValue) public var lineSpacing: Double = 1.2 - @AppStorage(ThemeKey.statusActionSecondary.rawValue) public var statusActionSecondary: StatusActionSecondary = .share + @AppStorage(ThemeKey.statusActionSecondary.rawValue) public var statusActionSecondary: + StatusActionSecondary = .share @AppStorage(ThemeKey.contentGradient.rawValue) public var showContentGradient: Bool = true @AppStorage(ThemeKey.compactLayoutPadding.rawValue) public var compactLayoutPadding: Bool = true @AppStorage("font_size_scale") public var fontSizeScale: Double = 1 @@ -139,14 +146,17 @@ public final class Theme { return _cachedChoosenFont } guard let chosenFontData, - let font = try? NSKeyedUnarchiver.unarchivedObject(ofClass: UIFont.self, from: chosenFontData) else { return nil } + let font = try? NSKeyedUnarchiver.unarchivedObject( + ofClass: UIFont.self, from: chosenFontData) + else { return nil } _cachedChoosenFont = font return font } set { if let font = newValue, - let data = try? NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false) + let data = try? NSKeyedArchiver.archivedData( + withRootObject: font, requiringSecureCoding: false) { chosenFontData = data } else { @@ -292,13 +302,13 @@ public final class Theme { themeStorage.showContentGradient = showContentGradient } } - + public var compactLayoutPadding: Bool { didSet { themeStorage.compactLayoutPadding = compactLayoutPadding } } - + public var selectedSet: ColorSetName = .iceCubeDark public static let shared = Theme() @@ -327,7 +337,7 @@ public final class Theme { primaryBackgroundColor = themeStorage.primaryBackgroundColor secondaryBackgroundColor = themeStorage.secondaryBackgroundColor labelColor = themeStorage.labelColor - contrastingTintColor = .red // real work done in computeContrastingTintColor() + contrastingTintColor = .red // real work done in computeContrastingTintColor() avatarPosition = themeStorage.avatarPosition avatarShape = themeStorage.avatarShape storedSet = themeStorage.storedSet diff --git a/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift b/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift index 2612d66e..0861855b 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift @@ -1,10 +1,11 @@ import SwiftUI + #if canImport(UIKit) import UIKit #endif -public extension View { - @MainActor func applyTheme(_ theme: Theme) -> some View { +extension View { + @MainActor public func applyTheme(_ theme: Theme) -> some View { modifier(ThemeApplier(theme: theme)) } } @@ -26,40 +27,46 @@ struct ThemeApplier: ViewModifier { content .tint(theme.tintColor) .preferredColorScheme(actualColorScheme) - #if canImport(UIKit) - .onAppear { - // If theme is never set before set the default store. This should only execute once after install. - if !theme.isThemePreviouslySet { - theme.applySet(set: colorScheme == .dark ? .iceCubeDark : .iceCubeLight) - theme.isThemePreviouslySet = true - } else if theme.followSystemColorScheme, theme.isThemePreviouslySet, - let sets = availableColorsSets - .first(where: { $0.light.name == theme.selectedSet || $0.dark.name == theme.selectedSet }) - { - theme.applySet(set: colorScheme == .dark ? sets.dark.name : sets.light.name) + #if canImport(UIKit) + .onAppear { + // If theme is never set before set the default store. This should only execute once after install. + if !theme.isThemePreviouslySet { + theme.applySet(set: colorScheme == .dark ? .iceCubeDark : .iceCubeLight) + theme.isThemePreviouslySet = true + } else if theme.followSystemColorScheme, theme.isThemePreviouslySet, + let sets = + availableColorsSets + .first(where: { + $0.light.name == theme.selectedSet || $0.dark.name == theme.selectedSet + }) + { + theme.applySet(set: colorScheme == .dark ? sets.dark.name : sets.light.name) + } + setWindowTint(theme.tintColor) + setWindowUserInterfaceStyle(from: theme.selectedScheme) + setBarsColor(theme.primaryBackgroundColor) } - setWindowTint(theme.tintColor) - setWindowUserInterfaceStyle(from: theme.selectedScheme) - setBarsColor(theme.primaryBackgroundColor) - } - .onChange(of: theme.tintColor) { _, newValue in - setWindowTint(newValue) - } - .onChange(of: theme.primaryBackgroundColor) { _, newValue in - setBarsColor(newValue) - } - .onChange(of: theme.selectedScheme) { _, newValue in - setWindowUserInterfaceStyle(from: newValue) - } - .onChange(of: colorScheme) { _, newColorScheme in - if theme.followSystemColorScheme, - let sets = availableColorsSets - .first(where: { $0.light.name == theme.selectedSet || $0.dark.name == theme.selectedSet }) - { - theme.applySet(set: newColorScheme == .dark ? sets.dark.name : sets.light.name) + .onChange(of: theme.tintColor) { _, newValue in + setWindowTint(newValue) } - } - #endif + .onChange(of: theme.primaryBackgroundColor) { _, newValue in + setBarsColor(newValue) + } + .onChange(of: theme.selectedScheme) { _, newValue in + setWindowUserInterfaceStyle(from: newValue) + } + .onChange(of: colorScheme) { _, newColorScheme in + if theme.followSystemColorScheme, + let sets = + availableColorsSets + .first(where: { + $0.light.name == theme.selectedSet || $0.dark.name == theme.selectedSet + }) + { + theme.applySet(set: newColorScheme == .dark ? sets.dark.name : sets.light.name) + } + } + #endif } #if canImport(UIKit) diff --git a/Packages/DesignSystem/Sources/DesignSystem/ToolbarItem/CloseToolbarItem.swift b/Packages/DesignSystem/Sources/DesignSystem/ToolbarItem/CloseToolbarItem.swift index 36c4bd09..5c053862 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/ToolbarItem/CloseToolbarItem.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/ToolbarItem/CloseToolbarItem.swift @@ -7,11 +7,14 @@ public struct CloseToolbarItem: ToolbarContent { public var body: some ToolbarContent { ToolbarItem(placement: .navigationBarLeading) { - Button(action: { - dismiss() - }, label: { - Image(systemName: "xmark.circle") - }) + Button( + action: { + dismiss() + }, + label: { + Image(systemName: "xmark.circle") + } + ) .keyboardShortcut(.cancelAction) } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/AccountPopoverView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/AccountPopoverView.swift index fb63f493..47609f2b 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/AccountPopoverView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/AccountPopoverView.swift @@ -7,7 +7,7 @@ import SwiftUI @MainActor struct AccountPopoverView: View { let account: Account - let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview + let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview private let config: AvatarView.FrameConfig = .account @Binding var showPopup: Bool @@ -16,7 +16,8 @@ struct AccountPopoverView: View { var body: some View { VStack(alignment: .leading) { - LazyImage(request: ImageRequest(url: account.header) + LazyImage( + request: ImageRequest(url: account.header) ) { state in if let image = state.image { image.resizable().scaledToFill() @@ -96,7 +97,9 @@ struct AccountPopoverView: View { } @MainActor - private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) -> some View { + private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) + -> some View + { VStack { Text(count, format: .number.notation(.compactName)) .font(.scaledHeadline) @@ -112,9 +115,11 @@ struct AccountPopoverView: View { Text(title) .font(.scaledFootnote) .foregroundStyle(.secondary) - .alignmentGuide(.bottomAvatar, computeValue: { dimension in - dimension[.firstTextBaseline] - }) + .alignmentGuide( + .bottomAvatar, + computeValue: { dimension in + dimension[.firstTextBaseline] + }) } .accessibilityElement(children: .ignore) .accessibilityLabel(title) @@ -122,12 +127,14 @@ struct AccountPopoverView: View { } private var adaptiveConfig: AvatarView.FrameConfig { - let cornerRadius: CGFloat = if config == .badge || theme.avatarShape == .circle { - config.width / 2 - } else { - config.cornerRadius - } - return AvatarView.FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius) + let cornerRadius: CGFloat = + if config == .badge || theme.avatarShape == .circle { + config.width / 2 + } else { + config.cornerRadius + } + return AvatarView.FrameConfig( + width: config.width, height: config.height, cornerRadius: cornerRadius) } } @@ -156,33 +163,34 @@ public struct AccountPopoverModifier: ViewModifier { return AnyView(content) } - return AnyView(content - .onHover { hovering in - if hovering { - toggleTask.cancel() - toggleTask = Task { - try? await Task.sleep(nanoseconds: NSEC_PER_SEC / 2) - guard !Task.isCancelled else { return } + return AnyView( + content + .onHover { hovering in + if hovering { + toggleTask.cancel() + toggleTask = Task { + try? await Task.sleep(nanoseconds: NSEC_PER_SEC / 2) + guard !Task.isCancelled else { return } + if !showPopup { + showPopup = true + } + } + } else { if !showPopup { - showPopup = true + toggleTask.cancel() } } - } else { - if !showPopup { - toggleTask.cancel() - } } - } - .hoverEffect(.lift) - .popover(isPresented: $showPopup) { - AccountPopoverView( - account: account, - theme: theme, - showPopup: $showPopup, - autoDismiss: $autoDismiss, - toggleTask: $toggleTask - ) - }) + .hoverEffect(.lift) + .popover(isPresented: $showPopup) { + AccountPopoverView( + account: account, + theme: theme, + showPopup: $showPopup, + autoDismiss: $autoDismiss, + toggleTask: $toggleTask + ) + }) } init(_ account: Account) { @@ -190,8 +198,8 @@ public struct AccountPopoverModifier: ViewModifier { } } -public extension View { - func accountPopover(_ account: Account) -> some View { +extension View { + public func accountPopover(_ account: Account) -> some View { modifier(AccountPopoverModifier(account)) } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift index 8625ec80..575cb26e 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift @@ -20,11 +20,12 @@ public struct AvatarView: View { } private var adaptiveConfig: FrameConfig { - let cornerRadius: CGFloat = if config == .badge || theme.avatarShape == .circle { - config.width / 2 - } else { - config.cornerRadius - } + let cornerRadius: CGFloat = + if config == .badge || theme.avatarShape == .circle { + config.width / 2 + } else { + config.cornerRadius + } return FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius) } @@ -88,10 +89,14 @@ struct PreviewWrapper: View { id: UUID().uuidString, username: "@clattner_llvm", displayName: "Chris Lattner", - avatar: URL(string: "https://pbs.twimg.com/profile_images/1484209565788897285/1n6Viahb_400x400.jpg")!, + avatar: URL( + string: "https://pbs.twimg.com/profile_images/1484209565788897285/1n6Viahb_400x400.jpg")!, header: URL(string: "https://pbs.twimg.com/profile_banners/2543588034/1656822255/1500x500")!, acct: "clattner_llvm@example.com", - note: .init(stringValue: "Building beautiful things @Modular_AI 🔥, lifting the world of production AI/ML software into a new phase of innovation. We’re hiring! 🚀🧠"), + note: .init( + stringValue: + "Building beautiful things @Modular_AI 🔥, lifting the world of production AI/ML software into a new phase of innovation. We’re hiring! 🚀🧠" + ), createdAt: ServerDate(), followersCount: 77100, followingCount: 167, @@ -117,7 +122,8 @@ struct AvatarImage: View { if reasons == .placeholder { AvatarPlaceHolder(config: config) } else { - LazyImage(request: ImageRequest(url: avatar, processors: [.resize(size: config.size)]) + LazyImage( + request: ImageRequest(url: avatar, processors: [.resize(size: config.size)]) ) { state in if let image = state.image { image diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift index 15895d86..5b6a9142 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift @@ -10,7 +10,10 @@ public struct EmojiTextApp: View { private let append: (() -> Text)? private let lineLimit: Int? - public init(_ markdown: HTMLString, emojis: [Emoji], language: String? = nil, lineLimit: Int? = nil, append: (() -> Text)? = nil) { + public init( + _ markdown: HTMLString, emojis: [Emoji], language: String? = nil, lineLimit: Int? = nil, + append: (() -> Text)? = nil + ) { self.markdown = markdown self.emojis = emojis.map { RemoteEmoji(shortcode: $0.shortcode, url: $0.url) } self.language = language diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift index f0fcf639..0af06574 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift @@ -6,7 +6,10 @@ public struct ErrorView: View { public let buttonTitle: LocalizedStringKey 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 @@ -46,7 +49,9 @@ public struct ErrorView: View { } #Preview { - ErrorView(title: "Error", - message: "Error loading. Please try again", - buttonTitle: "Retry") {} + ErrorView( + title: "Error", + message: "Error loading. Please try again", + buttonTitle: "Retry" + ) {} } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/LazyResizableImage.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/LazyResizableImage.swift index 5b06a46b..d239b655 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/LazyResizableImage.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/LazyResizableImage.swift @@ -12,7 +12,8 @@ import SwiftUI /// A LazyImage (Nuke) with a geometry reader under the hood in order to use a Resize Processor to optimize performances on lists. /// This views also allows smooth resizing of the images by debouncing the update of the ImageProcessor. public struct LazyResizableImage: View { - public init(url: URL?, @ViewBuilder content: @escaping (LazyImageState, GeometryProxy) -> Content) { + public init(url: URL?, @ViewBuilder content: @escaping (LazyImageState, GeometryProxy) -> Content) + { imageURL = url self.content = content } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/PlaceholderView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/PlaceholderView.swift index 35488b2e..c2850133 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/PlaceholderView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/PlaceholderView.swift @@ -12,14 +12,16 @@ public struct PlaceholderView: View { } public var body: some View { - ContentUnavailableView(title, - systemImage: iconName, - description: Text(message)) + ContentUnavailableView( + title, + systemImage: iconName, + description: Text(message)) } } #Preview { - PlaceholderView(iconName: "square.and.arrow.up.trianglebadge.exclamationmark", - title: "Nothing to see", - message: "This is a preview. Please try again.") + PlaceholderView( + iconName: "square.and.arrow.up.trianglebadge.exclamationmark", + title: "Nothing to see", + message: "This is a preview. Please try again.") } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift index 0360b10d..8743d6d6 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift @@ -3,19 +3,21 @@ import Models import SwiftUI @MainActor -public extension View { - func statusEditorToolbarItem(routerPath _: RouterPath, - visibility: Models.Visibility) -> some ToolbarContent - { +extension View { + public func statusEditorToolbarItem( + routerPath _: RouterPath, + visibility: Models.Visibility + ) -> some ToolbarContent { StatusEditorToolbarItem(visibility: visibility) } } @MainActor -public extension ToolbarContent { - func statusEditorToolbarItem(routerPath _: RouterPath, - visibility: Models.Visibility) -> some ToolbarContent - { +extension ToolbarContent { + public func statusEditorToolbarItem( + routerPath _: RouterPath, + visibility: Models.Visibility + ) -> some ToolbarContent { StatusEditorToolbarItem(visibility: visibility) } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/TagChartView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/TagChartView.swift index 8ca93d22..9728d068 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/TagChartView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/TagChartView.swift @@ -6,16 +6,19 @@ public struct TagChartView: View { @State private var sortedHistory: [History] = [] public init(tag: Tag) { - _sortedHistory = .init(initialValue: tag.history.sorted { - Int($0.day) ?? 0 < Int($1.day) ?? 0 - }) + _sortedHistory = .init( + initialValue: tag.history.sorted { + Int($0.day) ?? 0 < Int($1.day) ?? 0 + }) } public var body: some View { Chart(sortedHistory) { data in - AreaMark(x: .value("day", sortedHistory.firstIndex(where: { $0.id == data.id }) ?? 0), - y: .value("uses", Int(data.uses) ?? 0)) - .interpolationMethod(.catmullRom) + AreaMark( + x: .value("day", sortedHistory.firstIndex(where: { $0.id == data.id }) ?? 0), + y: .value("uses", Int(data.uses) ?? 0) + ) + .interpolationMethod(.catmullRom) } .chartLegend(.hidden) .chartXAxis(.hidden) diff --git a/Packages/Env/Package.swift b/Packages/Env/Package.swift index f02cdfe6..bff64fd0 100644 --- a/Packages/Env/Package.swift +++ b/Packages/Env/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "Env", targets: ["Env"] - ), + ) ], dependencies: [ .package(name: "Models", path: "../Models"), @@ -29,10 +29,10 @@ let package = Package( .product(name: "Models", package: "Models"), .product(name: "Network", package: "Network"), .product(name: "KeychainSwift", package: "keychain-swift"), - .product(name: "TelemetryDeck", package: "SwiftSDK") + .product(name: "TelemetryDeck", package: "SwiftSDK"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] ), .testTarget( diff --git a/Packages/Env/Sources/Env/Duration.swift b/Packages/Env/Sources/Env/Duration.swift index eb3547f6..367476f2 100644 --- a/Packages/Env/Sources/Env/Duration.swift +++ b/Packages/Env/Sources/Env/Duration.swift @@ -46,6 +46,9 @@ public enum Duration: Int, CaseIterable { } public static func pollDurations() -> [Duration] { - [.fiveMinutes, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .threeDays, .sevenDays] + [ + .fiveMinutes, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .threeDays, + .sevenDays, + ] } } diff --git a/Packages/Env/Sources/Env/Ext/AppStorage.swift b/Packages/Env/Sources/Env/Ext/AppStorage.swift index 2fa804a5..02be2ddd 100644 --- a/Packages/Env/Sources/Env/Ext/AppStorage.swift +++ b/Packages/Env/Sources/Env/Ext/AppStorage.swift @@ -3,7 +3,7 @@ import Foundation extension Array: @retroactive RawRepresentable where Element: Codable { public init?(rawValue: String) { guard let data = rawValue.data(using: .utf8), - let result = try? JSONDecoder().decode([Element].self, from: data) + let result = try? JSONDecoder().decode([Element].self, from: data) else { return nil } @@ -12,7 +12,7 @@ extension Array: @retroactive RawRepresentable where Element: Codable { public var rawValue: String { guard let data = try? JSONEncoder().encode(self), - let result = String(data: data, encoding: .utf8) + let result = String(data: data, encoding: .utf8) else { return "[]" } diff --git a/Packages/Env/Sources/Env/Ext/TranslationView.swift b/Packages/Env/Sources/Env/Ext/TranslationView.swift index 5b6e3a42..271424da 100644 --- a/Packages/Env/Sources/Env/Ext/TranslationView.swift +++ b/Packages/Env/Sources/Env/Ext/TranslationView.swift @@ -3,16 +3,16 @@ import SwiftUI #if canImport(_Translation_SwiftUI) import Translation - public extension View { - func addTranslateView(isPresented: Binding, text: String) -> some View { + extension View { + public func addTranslateView(isPresented: Binding, text: String) -> some View { #if targetEnvironment(macCatalyst) || os(visionOS) return self #else - if #available(iOS 17.4, *) { - return self.translationPresentation(isPresented: isPresented, text: text) - } else { - return self - } + if #available(iOS 17.4, *) { + return self.translationPresentation(isPresented: isPresented, text: text) + } else { + return self + } #endif } } diff --git a/Packages/Env/Sources/Env/NotificationsName.swift b/Packages/Env/Sources/Env/NotificationsName.swift index d740f7f8..7b2a2d09 100644 --- a/Packages/Env/Sources/Env/NotificationsName.swift +++ b/Packages/Env/Sources/Env/NotificationsName.swift @@ -1,10 +1,10 @@ import UIKit -public extension Notification.Name { - static let shareSheetClose = NSNotification.Name("shareSheetClose") - static let refreshTimeline = Notification.Name("refreshTimeline") - static let homeTimeline = Notification.Name("homeTimeline") - static let trendingTimeline = Notification.Name("trendingTimeline") - static let federatedTimeline = Notification.Name("federatedTimeline") - static let localTimeline = Notification.Name("localTimeline") +extension Notification.Name { + public static let shareSheetClose = NSNotification.Name("shareSheetClose") + public static let refreshTimeline = Notification.Name("refreshTimeline") + public static let homeTimeline = Notification.Name("homeTimeline") + public static let trendingTimeline = Notification.Name("trendingTimeline") + public static let federatedTimeline = Notification.Name("federatedTimeline") + public static let localTimeline = Notification.Name("localTimeline") } diff --git a/Packages/Env/Sources/Env/PreviewEnv.swift b/Packages/Env/Sources/Env/PreviewEnv.swift index be0d9b7a..d078eebc 100644 --- a/Packages/Env/Sources/Env/PreviewEnv.swift +++ b/Packages/Env/Sources/Env/PreviewEnv.swift @@ -2,8 +2,8 @@ import Network import SwiftUI @MainActor -public extension View { - func withPreviewsEnv() -> some View { +extension View { + public func withPreviewsEnv() -> some View { environment(RouterPath()) .environment(Client(server: "")) .environment(CurrentAccount.shared) diff --git a/Packages/Env/Sources/Env/PushNotificationsService.swift b/Packages/Env/Sources/Env/PushNotificationsService.swift index 54ce9bf5..c06c3334 100644 --- a/Packages/Env/Sources/Env/PushNotificationsService.swift +++ b/Packages/Env/Sources/Env/PushNotificationsService.swift @@ -17,9 +17,9 @@ public struct PushKeys: Sendable { static let keychainAuthKey = "notifications_auth_key" static let keychainPrivateKey = "notifications_private_key" } - - public init() { } - + + public init() {} + private var keychain: KeychainSwift { let keychain = KeychainSwift() #if !DEBUG && !targetEnvironment(simulator) @@ -27,47 +27,52 @@ public struct PushKeys: Sendable { #endif return keychain } - + public var notificationsPrivateKeyAsKey: P256.KeyAgreement.PrivateKey { if let key = keychain.get(Constants.keychainPrivateKey), - let data = Data(base64Encoded: key) + let data = Data(base64Encoded: key) { do { return try P256.KeyAgreement.PrivateKey(rawRepresentation: data) } catch { let key = P256.KeyAgreement.PrivateKey() - keychain.set(key.rawRepresentation.base64EncodedString(), - forKey: Constants.keychainPrivateKey, - withAccess: .accessibleAfterFirstUnlock) + keychain.set( + key.rawRepresentation.base64EncodedString(), + forKey: Constants.keychainPrivateKey, + withAccess: .accessibleAfterFirstUnlock) return key } } else { let key = P256.KeyAgreement.PrivateKey() - keychain.set(key.rawRepresentation.base64EncodedString(), - forKey: Constants.keychainPrivateKey, - withAccess: .accessibleAfterFirstUnlock) + keychain.set( + key.rawRepresentation.base64EncodedString(), + forKey: Constants.keychainPrivateKey, + withAccess: .accessibleAfterFirstUnlock) return key } } public var notificationsAuthKeyAsKey: Data { if let key = keychain.get(Constants.keychainAuthKey), - let data = Data(base64Encoded: key) + let data = Data(base64Encoded: key) { return data } else { let key = Self.makeRandomNotificationsAuthKey() - keychain.set(key.base64EncodedString(), - forKey: Constants.keychainAuthKey, - withAccess: .accessibleAfterFirstUnlock) + keychain.set( + key.base64EncodedString(), + forKey: Constants.keychainAuthKey, + withAccess: .accessibleAfterFirstUnlock) return key } } - + private static func makeRandomNotificationsAuthKey() -> Data { let byteCount = 16 var bytes = Data(count: byteCount) - _ = bytes.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, byteCount, $0.baseAddress!) } + _ = bytes.withUnsafeMutableBytes { + SecRandomCopyBytes(kSecRandomDefault, byteCount, $0.baseAddress!) + } return bytes } } @@ -98,7 +103,7 @@ public struct HandledNotification: Equatable { public static let shared = PushNotificationsService() private let pushKeys = PushKeys() - + public private(set) var subscriptions: [PushNotificationSubscriptionSettings] = [] public var pushToken: Data? @@ -112,7 +117,8 @@ public struct HandledNotification: Equatable { } public func requestPushNotifications() { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { @Sendable _, _ in + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { + @Sendable _, _ in DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } @@ -122,49 +128,60 @@ public struct HandledNotification: Equatable { public func setAccounts(accounts: [PushAccount]) { subscriptions = [] for account in accounts { - let sub = PushNotificationSubscriptionSettings(account: account, - key: pushKeys.notificationsPrivateKeyAsKey.publicKey.x963Representation, - authKey: pushKeys.notificationsAuthKeyAsKey, - pushToken: pushToken) + let sub = PushNotificationSubscriptionSettings( + account: account, + key: pushKeys.notificationsPrivateKeyAsKey.publicKey.x963Representation, + authKey: pushKeys.notificationsAuthKeyAsKey, + pushToken: pushToken) subscriptions.append(sub) } } public func updateSubscriptions(forceCreate: Bool) async { for subscription in subscriptions { - await withTaskGroup(of: Void.self, body: { group in - group.addTask { - await subscription.fetchSubscription() - if await subscription.subscription != nil, !forceCreate { - await subscription.deleteSubscription() - await subscription.updateSubscription() - } else if forceCreate { - await subscription.updateSubscription() + await withTaskGroup( + of: Void.self, + body: { group in + group.addTask { + await subscription.fetchSubscription() + if await subscription.subscription != nil, !forceCreate { + await subscription.deleteSubscription() + await subscription.updateSubscription() + } else if forceCreate { + await subscription.updateSubscription() + } } - } - }) + }) } } } extension PushNotificationsService: UNUserNotificationCenterDelegate { - public func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { + public func userNotificationCenter( + _: UNUserNotificationCenter, didReceive response: UNNotificationResponse + ) async { guard let plaintext = response.notification.request.content.userInfo["plaintext"] as? Data, - let mastodonPushNotification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintext), - let account = subscriptions.first(where: { $0.account.token.accessToken == mastodonPushNotification.accessToken }) + let mastodonPushNotification = try? JSONDecoder().decode( + MastodonPushNotification.self, from: plaintext), + let account = subscriptions.first(where: { + $0.account.token.accessToken == mastodonPushNotification.accessToken + }) else { return } do { let client = Client(server: account.account.server, oauthToken: account.account.token) let notification: Models.Notification = - try await client.get(endpoint: Notifications.notification(id: String(mastodonPushNotification.notificationID))) + try await client.get( + endpoint: Notifications.notification(id: String(mastodonPushNotification.notificationID))) handledNotification = .init(account: account.account, notification: notification) } catch {} } - - public func userNotificationCenter(_ center: UNUserNotificationCenter, - willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification + ) async -> UNNotificationPresentationOptions { return [.banner, .sound] } } @@ -224,15 +241,17 @@ extension Data { listenerURL += "?sandbox=true" #endif subscription = - try await client.post(endpoint: Push.createSub(endpoint: listenerURL, - p256dh: key, - auth: authKey, - mentions: isMentionNotificationEnabled, - status: isNewPostsNotificationEnabled, - reblog: isReblogNotificationEnabled, - follow: isFollowNotificationEnabled, - favorite: isFavoriteNotificationEnabled, - poll: isPollNotificationEnabled)) + try await client.post( + endpoint: Push.createSub( + endpoint: listenerURL, + p256dh: key, + auth: authKey, + mentions: isMentionNotificationEnabled, + status: isNewPostsNotificationEnabled, + reblog: isReblogNotificationEnabled, + follow: isFollowNotificationEnabled, + favorite: isFavoriteNotificationEnabled, + poll: isPollNotificationEnabled)) isEnabled = subscription != nil } catch { diff --git a/Packages/Env/Sources/Env/QuickLook.swift b/Packages/Env/Sources/Env/QuickLook.swift index 06980c60..ecb5d99a 100644 --- a/Packages/Env/Sources/Env/QuickLook.swift +++ b/Packages/Env/Sources/Env/QuickLook.swift @@ -11,7 +11,9 @@ import QuickLook private init() {} - public func prepareFor(selectedMediaAttachment: MediaAttachment, mediaAttachments: [MediaAttachment]) { + public func prepareFor( + selectedMediaAttachment: MediaAttachment, mediaAttachments: [MediaAttachment] + ) { self.selectedMediaAttachment = selectedMediaAttachment self.mediaAttachments = mediaAttachments } diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index 632483f8..6534fec4 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -83,7 +83,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" @@ -147,8 +147,8 @@ public enum SettingsStartingPoint { public func handleStatus(status: AnyStatus, url: URL) -> OpenURLAction.Result { if url.pathComponents.count == 3, url.pathComponents[1] == "tags", - url.host() == status.account.url?.host(), - let tag = url.pathComponents.last + url.host() == status.account.url?.host(), + let tag = url.pathComponents.last { // OK this test looks weird but it's // A 3 component path i.e. ["/", "tags", "tagname"] @@ -161,9 +161,9 @@ public enum SettingsStartingPoint { navigate(to: .accountDetail(id: mention.id)) 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 !StatusEmbedCache.shared.badStatusesURLs.contains(url) { if url.absoluteString.contains(client.server) { @@ -179,14 +179,14 @@ public enum SettingsStartingPoint { public func handle(url: URL) -> OpenURLAction.Result { if url.pathComponents.contains(where: { $0 == "tags" }), - let tag = url.pathComponents.last + let tag = url.pathComponents.last { navigate(to: .hashTag(tag: tag, account: nil)) return .handled - } else if url.lastPathComponent.first == "@" || - (url.host() == AppInfo.premiumInstance && url.pathComponents.contains("users")), - let host = url.host, - !host.hasPrefix("www") + } else if url.lastPathComponent.first == "@" + || (url.host() == AppInfo.premiumInstance && url.pathComponents.contains("users")), + let host = url.host, + !host.hasPrefix("www") { let acct = "\(url.lastPathComponent)@\(host)" Task { @@ -194,9 +194,9 @@ public enum SettingsStartingPoint { } 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))) @@ -210,8 +210,8 @@ public enum SettingsStartingPoint { public func handleDeepLink(url: URL) -> OpenURLAction.Result { guard let client, - client.isAuth, - let id = Int(url.lastPathComponent) + client.isAuth, + let id = Int(url.lastPathComponent) else { return urlHandler?(url) ?? .systemAction } @@ -261,11 +261,13 @@ public enum SettingsStartingPoint { public func navigateToAccountFrom(acct: String, url: URL) async { guard let client else { return } - let results: SearchResults? = try? await client.get(endpoint: Search.search(query: acct, - type: .accounts, - offset: nil, - following: nil), - forceVersion: .v2) + let results: SearchResults? = try? await client.get( + endpoint: Search.search( + query: acct, + type: .accounts, + offset: nil, + following: nil), + forceVersion: .v2) if let account = results?.accounts.first { navigate(to: .accountDetailWithAccount(account: account)) } else { @@ -275,11 +277,13 @@ public enum SettingsStartingPoint { public func navigateToAccountFrom(url: URL) async { guard let client else { return } - let results: SearchResults? = try? await client.get(endpoint: Search.search(query: url.absoluteString, - type: .accounts, - offset: nil, - following: nil), - forceVersion: .v2) + let results: SearchResults? = try? await client.get( + endpoint: Search.search( + query: url.absoluteString, + type: .accounts, + offset: nil, + following: nil), + forceVersion: .v2) if let account = results?.accounts.first { navigate(to: .accountDetailWithAccount(account: account)) } else { diff --git a/Packages/Env/Sources/Env/SoundEffectManager.swift b/Packages/Env/Sources/Env/SoundEffectManager.swift index f9f54be8..c65aeff5 100644 --- a/Packages/Env/Sources/Env/SoundEffectManager.swift +++ b/Packages/Env/Sources/Env/SoundEffectManager.swift @@ -1,5 +1,5 @@ -import AudioToolbox import AVKit +import AudioToolbox import CoreHaptics import UIKit @@ -21,7 +21,9 @@ public class SoundEffectManager { private func registerSounds() { SoundEffect.allCases.forEach { effect in - guard let url = Bundle.main.url(forResource: effect.rawValue, withExtension: "wav") else { return } + guard let url = Bundle.main.url(forResource: effect.rawValue, withExtension: "wav") else { + return + } register(url: url, for: effect) } } diff --git a/Packages/Env/Sources/Env/StatusAction.swift b/Packages/Env/Sources/Env/StatusAction.swift index bf8a54ed..085674c9 100644 --- a/Packages/Env/Sources/Env/StatusAction.swift +++ b/Packages/Env/Sources/Env/StatusAction.swift @@ -7,7 +7,10 @@ public enum StatusAction: String, CaseIterable, Identifiable { case none, reply, boost, favorite, bookmark, quote - public func displayName(isReblogged: Bool = false, isFavorited: Bool = false, isBookmarked: Bool = false, privateBoost: Bool = false) -> LocalizedStringKey { + public func displayName( + isReblogged: Bool = false, isFavorited: Bool = false, isBookmarked: Bool = false, + privateBoost: Bool = false + ) -> LocalizedStringKey { switch self { case .none: return "settings.swipeactions.status.action.none" @@ -22,13 +25,18 @@ public enum StatusAction: String, CaseIterable, Identifiable { return isReblogged ? "status.action.unboost" : "settings.swipeactions.status.action.boost" case .favorite: - return isFavorited ? "status.action.unfavorite" : "settings.swipeactions.status.action.favorite" + return isFavorited + ? "status.action.unfavorite" : "settings.swipeactions.status.action.favorite" case .bookmark: - return isBookmarked ? "status.action.unbookmark" : "settings.swipeactions.status.action.bookmark" + return isBookmarked + ? "status.action.unbookmark" : "settings.swipeactions.status.action.bookmark" } } - public func iconName(isReblogged: Bool = false, isFavorited: Bool = false, isBookmarked: Bool = false, privateBoost: Bool = false) -> String { + public func iconName( + isReblogged: Bool = false, isFavorited: Bool = false, isBookmarked: Bool = false, + privateBoost: Bool = false + ) -> String { switch self { case .none: return "slash.circle" diff --git a/Packages/Env/Sources/Env/StreamWatcher.swift b/Packages/Env/Sources/Env/StreamWatcher.swift index 33c2bace..8d66de44 100644 --- a/Packages/Env/Sources/Env/StreamWatcher.swift +++ b/Packages/Env/Sources/Env/StreamWatcher.swift @@ -2,8 +2,8 @@ import Combine import Foundation import Models import Network -import Observation import OSLog +import Observation @MainActor @Observable public class StreamWatcher { @@ -46,10 +46,12 @@ import OSLog } private func connect() { - guard let task = try? client?.makeWebSocketTask( - endpoint: Streaming.streaming, - instanceStreamingURL: instanceStreamingURL - ) else { + guard + let task = try? client?.makeWebSocketTask( + endpoint: Streaming.streaming, + instanceStreamingURL: instanceStreamingURL + ) + else { return } self.task = task @@ -77,7 +79,7 @@ import OSLog private func sendMessage(message: StreamMessage) { if let encodedMessage = try? encoder.encode(message), - let stringMessage = String(data: encodedMessage, encoding: .utf8) + let stringMessage = String(data: encodedMessage, encoding: .utf8) { task?.send(.string(stringMessage), completionHandler: { _ in }) } @@ -101,7 +103,9 @@ import OSLog if let event = self.rawEventToEvent(rawEvent: rawEvent) { self.events.append(event) self.latestEvent = event - if let event = event as? StreamEventNotification, event.notification.status?.visibility != .direct { + if let event = event as? StreamEventNotification, + event.notification.status?.visibility != .direct + { self.unreadNotificationsCount += 1 } } diff --git a/Packages/Env/Sources/Env/Telemetry.swift b/Packages/Env/Sources/Env/Telemetry.swift index 6e44a298..6c175fe6 100644 --- a/Packages/Env/Sources/Env/Telemetry.swift +++ b/Packages/Env/Sources/Env/Telemetry.swift @@ -1,5 +1,5 @@ -import TelemetryDeck import SwiftUI +import TelemetryDeck @MainActor public class Telemetry { @@ -7,8 +7,7 @@ public class Telemetry { let config = TelemetryDeck.Config(appID: "F04175D2-599A-4504-867E-CE870B991EB7") TelemetryDeck.initialize(config: config) } - - + public static func signal(_ event: String, parameters: [String: String] = [:]) { TelemetryDeck.signal(event, parameters: parameters) } diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index 6fe323c5..d01cf7c1 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -18,14 +18,18 @@ import SwiftUI @AppStorage("use_instance_content_settings") public var useInstanceContentSettings: Bool = true @AppStorage("app_auto_expand_spoilers") public var appAutoExpandSpoilers = false - @AppStorage("app_auto_expand_media") public var appAutoExpandMedia: ServerPreferences.AutoExpandMedia = .hideSensitive - @AppStorage("app_default_post_visibility") public var appDefaultPostVisibility: Models.Visibility = .pub - @AppStorage("app_default_reply_visibility") public var appDefaultReplyVisibility: Models.Visibility = .pub + @AppStorage("app_auto_expand_media") public var appAutoExpandMedia: + ServerPreferences.AutoExpandMedia = .hideSensitive + @AppStorage("app_default_post_visibility") public var appDefaultPostVisibility: + Models.Visibility = .pub + @AppStorage("app_default_reply_visibility") public var appDefaultReplyVisibility: + Models.Visibility = .pub @AppStorage("app_default_posts_sensitive") public var appDefaultPostsSensitive = false @AppStorage("app_require_alt_text") public var appRequireAltText = false @AppStorage("autoplay_video") public var autoPlayVideo = true @AppStorage("mute_video") public var muteVideo = true - @AppStorage("preferred_translation_type") public var preferredTranslationType = TranslationType.useServerIfPossible + @AppStorage("preferred_translation_type") public var preferredTranslationType = TranslationType + .useServerIfPossible @AppStorage("user_deepl_api_free") public var userDeeplAPIFree = true @AppStorage("auto_detect_post_language") public var autoDetectPostLanguage = true @@ -41,18 +45,24 @@ import SwiftUI @AppStorage("show_second_column_ipad") public var showiPadSecondaryColumn = true - @AppStorage("swipeactions-status-trailing-right") public var swipeActionsStatusTrailingRight = StatusAction.favorite - @AppStorage("swipeactions-status-trailing-left") public var swipeActionsStatusTrailingLeft = StatusAction.boost - @AppStorage("swipeactions-status-leading-left") public var swipeActionsStatusLeadingLeft = StatusAction.reply - @AppStorage("swipeactions-status-leading-right") public var swipeActionsStatusLeadingRight = StatusAction.none + @AppStorage("swipeactions-status-trailing-right") public var swipeActionsStatusTrailingRight = + StatusAction.favorite + @AppStorage("swipeactions-status-trailing-left") public var swipeActionsStatusTrailingLeft = + StatusAction.boost + @AppStorage("swipeactions-status-leading-left") public var swipeActionsStatusLeadingLeft = + StatusAction.reply + @AppStorage("swipeactions-status-leading-right") public var swipeActionsStatusLeadingRight = + StatusAction.none @AppStorage("swipeactions-use-theme-color") public var swipeActionsUseThemeColor = false - @AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = .iconWithText + @AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = + .iconWithText @AppStorage("requested_review") public var requestedReview = false @AppStorage("collapse-long-posts") public var collapseLongPosts = true - @AppStorage("share-button-behavior") public var shareButtonBehavior: PreferredShareButtonBehavior = .linkOnly + @AppStorage("share-button-behavior") public var shareButtonBehavior: + PreferredShareButtonBehavior = .linkOnly @AppStorage("fast_refresh") public var fastRefreshEnabled: Bool = false @@ -62,7 +72,7 @@ import SwiftUI @AppStorage("show_account_popover") public var showAccountPopover: Bool = true @AppStorage("sidebar_expanded") public var isSidebarExpanded: Bool = false - + @AppStorage("stream_new_posts") public var isPostsStreamingEnabled: Bool = false init() { @@ -79,7 +89,7 @@ import SwiftUI } #if canImport(_Translation_SwiftUI) if #unavailable(iOS 17.4), - preferredTranslationType == .useApple + preferredTranslationType == .useApple { preferredTranslationType = .useServerIfPossible } @@ -122,7 +132,9 @@ import SwiftUI } public var pendingLocation: Alignment { - let fromLeft = Locale.current.language.characterDirection == .leftToRight ? pendingShownLeft : !pendingShownLeft + let fromLeft = + Locale.current.language.characterDirection == .leftToRight + ? pendingShownLeft : !pendingShownLeft if pendingShownAtBottom { if fromLeft { return .bottomLeading @@ -359,7 +371,7 @@ import SwiftUI storage.isSidebarExpanded = isSidebarExpanded } } - + public var isPostsStreamingEnabled: Bool { didSet { storage.isPostsStreamingEnabled = isPostsStreamingEnabled @@ -413,7 +425,9 @@ import SwiftUI getMinVisibility(getReplyVisibility(), status.visibility) } - private func getMinVisibility(_ vis1: Models.Visibility, _ vis2: Models.Visibility) -> Models.Visibility { + private func getMinVisibility(_ vis1: Models.Visibility, _ vis2: Models.Visibility) + -> Models.Visibility + { let no1 = Self.getIntOfVisibility(vis1) let no2 = Self.getIntOfVisibility(vis2) @@ -459,7 +473,8 @@ import SwiftUI public func reloadNotificationsCount(tokens: [OauthToken]) { notificationsCount = [:] for token in tokens { - notificationsCount[token] = Self.sharedDefault?.integer(forKey: "push_notifications_count_\(token.createdAt)") ?? 0 + notificationsCount[token] = + Self.sharedDefault?.integer(forKey: "push_notifications_count_\(token.createdAt)") ?? 0 } } diff --git a/Packages/Env/Tests/RouterTests.swift b/Packages/Env/Tests/RouterTests.swift index d1522beb..c09b5515 100644 --- a/Packages/Env/Tests/RouterTests.swift +++ b/Packages/Env/Tests/RouterTests.swift @@ -1,8 +1,9 @@ -@testable import Env import Network import SwiftUI -import XCTest import Testing +import XCTest + +@testable import Env @Test @MainActor @@ -26,8 +27,9 @@ func testRouterTagsURL() { @MainActor func testRouterLocalStatusURL() { let router = RouterPath() - let client = Client(server: "mastodon.social", - oauthToken: .init(accessToken: "", tokenType: "", scope: "", createdAt: 0)) + let client = Client( + server: "mastodon.social", + oauthToken: .init(accessToken: "", tokenType: "", scope: "", createdAt: 0)) client.addConnections(["mastodon.social"]) router.client = client let url = URL(string: "https://mastodon.social/status/1010384")! @@ -39,8 +41,9 @@ func testRouterLocalStatusURL() { @MainActor func testRouterRemoteStatusURL() { let router = RouterPath() - let client = Client(server: "mastodon.social", - oauthToken: .init(accessToken: "", tokenType: "", scope: "", createdAt: 0)) + let client = Client( + server: "mastodon.social", + oauthToken: .init(accessToken: "", tokenType: "", scope: "", createdAt: 0)) client.addConnections(["mastodon.social", "mastodon.online"]) router.client = client let url = URL(string: "https://mastodon.online/status/1010384")! diff --git a/Packages/Explore/Package.swift b/Packages/Explore/Package.swift index da8372ba..530108fd 100644 --- a/Packages/Explore/Package.swift +++ b/Packages/Explore/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "Explore", targets: ["Explore"] - ), + ) ], dependencies: [ .package(name: "Account", path: "../Account"), @@ -36,8 +36,8 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] - ), + ) ] ) diff --git a/Packages/Explore/Sources/Explore/ExploreView.swift b/Packages/Explore/Sources/Explore/ExploreView.swift index 792a92c8..f3dd2360 100644 --- a/Packages/Explore/Sources/Explore/ExploreView.swift +++ b/Packages/Explore/Sources/Explore/ExploreView.swift @@ -14,7 +14,7 @@ public struct ExploreView: View { @State private var viewModel = ExploreViewModel() - public init() { } + public init() {} public var body: some View { ScrollViewReader { proxy in @@ -28,11 +28,13 @@ public struct ExploreView: View { } else if !viewModel.searchQuery.isEmpty { if let results = viewModel.results[viewModel.searchQuery] { if results.isEmpty, !viewModel.isSearching { - PlaceholderView(iconName: "magnifyingglass", - title: "explore.search.empty.title", - message: "explore.search.empty.message") - .listRowBackground(theme.secondaryBackgroundColor) - .listRowSeparator(.hidden) + PlaceholderView( + iconName: "magnifyingglass", + title: "explore.search.empty.title", + message: "explore.search.empty.message" + ) + .listRowBackground(theme.secondaryBackgroundColor) + .listRowSeparator(.hidden) } else { makeSearchResultsView(results: results) } @@ -43,19 +45,21 @@ public struct ExploreView: View { Spacer() } #if !os(visionOS) - .listRowBackground(theme.secondaryBackgroundColor) + .listRowBackground(theme.secondaryBackgroundColor) #endif .listRowSeparator(.hidden) .id(UUID()) } } else if viewModel.allSectionsEmpty { - PlaceholderView(iconName: "magnifyingglass", - title: "explore.search.title", - message: "explore.search.message-\(client.server)") + PlaceholderView( + iconName: "magnifyingglass", + title: "explore.search.title", + message: "explore.search.message-\(client.server)" + ) #if !os(visionOS) .listRowBackground(theme.secondaryBackgroundColor) #endif - .listRowSeparator(.hidden) + .listRowSeparator(.hidden) } else { quickAccessView .padding(.bottom, 4) @@ -93,20 +97,22 @@ public struct ExploreView: View { .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) #endif - .navigationTitle("explore.navigation-title") - .navigationBarTitleDisplayMode(.inline) - .searchable(text: $viewModel.searchQuery, - isPresented: $viewModel.isSearchPresented, - placement: .navigationBarDrawer(displayMode: .always), - prompt: Text("explore.search.prompt")) - .searchScopes($viewModel.searchScope) { - ForEach(ExploreViewModel.SearchScope.allCases, id: \.self) { scope in - Text(scope.localizedString) - } - } - .task(id: viewModel.searchQuery) { - await viewModel.search() + .navigationTitle("explore.navigation-title") + .navigationBarTitleDisplayMode(.inline) + .searchable( + text: $viewModel.searchQuery, + isPresented: $viewModel.isSearchPresented, + placement: .navigationBarDrawer(displayMode: .always), + prompt: Text("explore.search.prompt") + ) + .searchScopes($viewModel.searchScope) { + ForEach(ExploreViewModel.SearchScope.allCases, id: \.self) { scope in + Text(scope.localizedString) } + } + .task(id: viewModel.searchQuery) { + await viewModel.search() + } } } @@ -122,7 +128,8 @@ public struct ExploreView: View { } .buttonStyle(.bordered) Button("explore.section.suggested-users") { - routerPath.navigate(to: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) + routerPath.navigate( + to: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) } .buttonStyle(.bordered) Button("explore.section.trending.tags") { @@ -137,15 +144,17 @@ public struct ExploreView: View { #if !os(visionOS) .listRowBackground(theme.secondaryBackgroundColor) #endif - .listRowSeparator(.hidden) + .listRowSeparator(.hidden) } private var loadingView: some View { ForEach(Status.placeholders()) { status in - StatusRowExternalView(viewModel: .init(status: status, client: client, routerPath: routerPath)) - .padding(.vertical, 8) - .redacted(reason: .placeholder) - .allowsHitTesting(false) + StatusRowExternalView( + viewModel: .init(status: status, client: client, routerPath: routerPath) + ) + .padding(.vertical, 8) + .redacted(reason: .placeholder) + .allowsHitTesting(false) #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) #endif @@ -154,18 +163,21 @@ public struct ExploreView: View { @ViewBuilder private func makeSearchResultsView(results: SearchResults) -> some View { - if !results.accounts.isEmpty, viewModel.searchScope == .all || viewModel.searchScope == .people { + if !results.accounts.isEmpty, viewModel.searchScope == .all || viewModel.searchScope == .people + { Section("explore.section.users") { ForEach(results.accounts) { account in if let relationship = results.relationships.first(where: { $0.id == account.id }) { AccountsListRow(viewModel: .init(account: account, relationShip: relationship)) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) - .listRowHoverEffectDisabled() - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #else + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) + .listRowHoverEffectDisabled() + #endif } } if viewModel.searchScope == .people { @@ -173,17 +185,21 @@ public struct ExploreView: View { } } } - if !results.hashtags.isEmpty, viewModel.searchScope == .all || viewModel.searchScope == .hashtags { + if !results.hashtags.isEmpty, + viewModel.searchScope == .all || viewModel.searchScope == .hashtags + { Section("explore.section.tags") { ForEach(results.hashtags) { tag in TagRowView(tag: tag) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) - .listRowHoverEffectDisabled() - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #else + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) + .listRowHoverEffectDisabled() + #endif .padding(.vertical, 4) } if viewModel.searchScope == .hashtags { @@ -194,15 +210,19 @@ public struct ExploreView: View { if !results.statuses.isEmpty, viewModel.searchScope == .all || viewModel.searchScope == .posts { Section("explore.section.posts") { ForEach(results.statuses) { status in - StatusRowExternalView(viewModel: .init(status: status, client: client, routerPath: routerPath)) + StatusRowExternalView( + viewModel: .init(status: status, client: client, routerPath: routerPath) + ) #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) .listRowHoverEffectDisabled() #endif - .padding(.vertical, 8) + .padding(.vertical, 8) } if viewModel.searchScope == .posts { makeNextPageView(for: .statuses) @@ -213,18 +233,24 @@ public struct ExploreView: View { private var suggestedAccountsSection: some View { Section("explore.section.suggested-users") { - ForEach(viewModel.suggestedAccounts - .prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) - { account in - if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) { + ForEach( + viewModel.suggestedAccounts + .prefix( + upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count) + ) { account in + if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { + $0.id == account.id + }) { AccountsListRow(viewModel: .init(account: account, relationShip: relationship)) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) - .listRowHoverEffectDisabled() - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #else + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) + .listRowHoverEffectDisabled() + #endif } } NavigationLink(value: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) { @@ -232,28 +258,33 @@ public struct ExploreView: View { .foregroundColor(theme.tintColor) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) - .listRowHoverEffectDisabled() + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) + .listRowHoverEffectDisabled() #endif } } private var trendingTagsSection: some View { Section("explore.section.trending.tags") { - ForEach(viewModel.trendingTags - .prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) - { tag in + ForEach( + viewModel.trendingTags + .prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count) + ) { tag in TagRowView(tag: tag) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) - .listRowHoverEffectDisabled() - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #else + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) + .listRowHoverEffectDisabled() + #endif .padding(.vertical, 4) } NavigationLink(value: RouterDestination.tagsList(tags: viewModel.trendingTags)) { @@ -261,29 +292,36 @@ public struct ExploreView: View { .foregroundColor(theme.tintColor) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) - .listRowHoverEffectDisabled() + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) + .listRowHoverEffectDisabled() #endif } } private var trendingPostsSection: some View { Section("explore.section.trending.posts") { - ForEach(viewModel.trendingStatuses - .prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) - { status in - StatusRowExternalView(viewModel: .init(status: status, client: client, routerPath: routerPath)) + ForEach( + viewModel.trendingStatuses + .prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count) + ) { status in + StatusRowExternalView( + viewModel: .init(status: status, client: client, routerPath: routerPath) + ) #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) .listRowHoverEffectDisabled() #endif - .padding(.vertical, 8) + .padding(.vertical, 8) } NavigationLink(value: RouterDestination.trendingTimeline) { @@ -291,29 +329,34 @@ public struct ExploreView: View { .foregroundColor(theme.tintColor) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) - .listRowHoverEffectDisabled() + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) + .listRowHoverEffectDisabled() #endif } } private var trendingLinksSection: some View { Section("explore.section.trending.links") { - ForEach(viewModel.trendingLinks - .prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) - { card in + ForEach( + viewModel.trendingLinks + .prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count) + ) { card in StatusRowCardView(card: card) .environment(\.isCompact, true) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) - .listRowHoverEffectDisabled() - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #else + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) + .listRowHoverEffectDisabled() + #endif .padding(.vertical, 8) } @@ -322,11 +365,13 @@ public struct ExploreView: View { .foregroundColor(theme.tintColor) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #else - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) - .listRowHoverEffectDisabled() + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) + .listRowHoverEffectDisabled() #endif } } diff --git a/Packages/Explore/Sources/Explore/ExploreViewModel.swift b/Packages/Explore/Sources/Explore/ExploreViewModel.swift index 6b995704..5563f582 100644 --- a/Packages/Explore/Sources/Explore/ExploreViewModel.swift +++ b/Packages/Explore/Sources/Explore/ExploreViewModel.swift @@ -36,7 +36,8 @@ import SwiftUI } var allSectionsEmpty: Bool { - trendingLinks.isEmpty && trendingTags.isEmpty && trendingStatuses.isEmpty && suggestedAccounts.isEmpty + trendingLinks.isEmpty && trendingTags.isEmpty && trendingStatuses.isEmpty + && suggestedAccounts.isEmpty } var searchQuery = "" { @@ -68,7 +69,8 @@ import SwiftUI trendingStatuses = data.trendingStatuses trendingLinks = data.trendingLinks - suggestedAccountsRelationShips = try await client.get(endpoint: Accounts.relationships(ids: suggestedAccounts.map(\.id))) + suggestedAccountsRelationShips = try await client.get( + endpoint: Accounts.relationships(ids: suggestedAccounts.map(\.id))) withAnimation { isLoaded = true } @@ -89,21 +91,24 @@ import SwiftUI async let trendingTags: [Tag] = client.get(endpoint: Trends.tags) async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses(offset: nil)) async let trendingLinks: [Card] = client.get(endpoint: Trends.links(offset: nil)) - return try await .init(suggestedAccounts: suggestedAccounts, - trendingTags: trendingTags, - trendingStatuses: trendingStatuses, - trendingLinks: trendingLinks) + return try await .init( + suggestedAccounts: suggestedAccounts, + trendingTags: trendingTags, + trendingStatuses: trendingStatuses, + trendingLinks: trendingLinks) } func search() async { guard let client, !searchQuery.isEmpty else { return } do { try await Task.sleep(for: .milliseconds(250)) - var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery, - type: nil, - offset: nil, - following: nil), - forceVersion: .v2) + var results: SearchResults = try await client.get( + endpoint: Search.search( + query: searchQuery, + type: nil, + offset: nil, + following: nil), + forceVersion: .v2) let relationships: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map(\.id))) results.relationships = relationships @@ -118,25 +123,30 @@ import SwiftUI func fetchNextPage(of type: Search.EntityType) async { guard let client, !searchQuery.isEmpty, - let results = results[searchQuery] else { return } + let results = results[searchQuery] + else { return } do { - let offset = switch type { - case .accounts: - results.accounts.count - case .hashtags: - results.hashtags.count - case .statuses: - results.statuses.count - } + let offset = + switch type { + case .accounts: + results.accounts.count + case .hashtags: + results.hashtags.count + case .statuses: + results.statuses.count + } - var newPageResults: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery, - type: type, - offset: offset, - following: nil), - forceVersion: .v2) + var newPageResults: SearchResults = try await client.get( + endpoint: Search.search( + query: searchQuery, + type: type, + offset: offset, + following: nil), + forceVersion: .v2) if type == .accounts { let relationships: [Relationship] = - try await client.get(endpoint: Accounts.relationships(ids: newPageResults.accounts.map(\.id))) + try await client.get( + endpoint: Accounts.relationships(ids: newPageResults.accounts.map(\.id))) newPageResults.relationships = relationships } diff --git a/Packages/Explore/Sources/Explore/TagsListView.swift b/Packages/Explore/Sources/Explore/TagsListView.swift index 7722b345..0d215beb 100644 --- a/Packages/Explore/Sources/Explore/TagsListView.swift +++ b/Packages/Explore/Sources/Explore/TagsListView.swift @@ -15,18 +15,18 @@ public struct TagsListView: View { List { ForEach(tags) { tag in TagRowView(tag: tag) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif .padding(.vertical, 4) } } #if !os(visionOS) - .scrollContentBackground(.hidden) - .background(theme.primaryBackgroundColor) - .listStyle(.plain) + .scrollContentBackground(.hidden) + .background(theme.primaryBackgroundColor) + .listStyle(.plain) #else - .listStyle(.grouped) + .listStyle(.grouped) #endif .navigationTitle("explore.section.trending.tags") .navigationBarTitleDisplayMode(.inline) diff --git a/Packages/Explore/Sources/Explore/TrendingLinksListView.swift b/Packages/Explore/Sources/Explore/TrendingLinksListView.swift index 79018d4a..2e77e83b 100644 --- a/Packages/Explore/Sources/Explore/TrendingLinksListView.swift +++ b/Packages/Explore/Sources/Explore/TrendingLinksListView.swift @@ -19,9 +19,9 @@ public struct TrendingLinksListView: View { ForEach(links) { card in StatusRowCardView(card: card) .environment(\.isCompact, true) - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif .padding(.vertical, 8) } NextPageView { @@ -29,13 +29,13 @@ public struct TrendingLinksListView: View { links.append(contentsOf: nextPage) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif } #if os(visionOS) - .listStyle(.insetGrouped) + .listStyle(.insetGrouped) #else - .listStyle(.plain) + .listStyle(.plain) #endif .refreshable { do { @@ -43,8 +43,8 @@ public struct TrendingLinksListView: View { } catch {} } #if !os(visionOS) - .scrollContentBackground(.hidden) - .background(theme.primaryBackgroundColor) + .scrollContentBackground(.hidden) + .background(theme.primaryBackgroundColor) #endif .navigationTitle("explore.section.trending.links") .navigationBarTitleDisplayMode(.inline) diff --git a/Packages/Lists/Package.swift b/Packages/Lists/Package.swift index 37d3c2a4..b68d0b8b 100644 --- a/Packages/Lists/Package.swift +++ b/Packages/Lists/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "Lists", targets: ["Lists"] - ), + ) ], dependencies: [ .package(name: "Account", path: "../Account"), @@ -34,8 +34,8 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] - ), + ) ] ) diff --git a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift index 522689eb..c63ad53d 100644 --- a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift +++ b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift @@ -21,17 +21,22 @@ public struct ListAddAccountView: View { List { ForEach(currentAccount.sortedLists) { list in HStack { - Toggle(list.title, isOn: .init(get: { - viewModel.inLists.contains(where: { $0.id == list.id }) - }, set: { value in - Task { - if value { - await viewModel.addToList(list: list) - } else { - await viewModel.removeFromList(list: list) - } - } - })) + Toggle( + list.title, + isOn: .init( + get: { + viewModel.inLists.contains(where: { $0.id == list.id }) + }, + set: { value in + Task { + if value { + await viewModel.addToList(list: list) + } else { + await viewModel.removeFromList(list: list) + } + } + }) + ) .disabled(viewModel.isLoadingInfo) Spacer() } diff --git a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift index e104f3b2..26b57a3c 100644 --- a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift +++ b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift @@ -31,7 +31,8 @@ import SwiftUI func addToList(list: Models.List) async { guard let client else { return } - let response = try? await client.post(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id])) + let response = try? await client.post( + endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id])) if response?.statusCode == 200 { inLists.append(list) } @@ -39,7 +40,8 @@ import SwiftUI func removeFromList(list: Models.List) async { guard let client else { return } - let response = try? await client.delete(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id])) + let response = try? await client.delete( + endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id])) if response?.statusCode == 200 { inLists.removeAll(where: { $0.id == list.id }) } diff --git a/Packages/Lists/Sources/Lists/Create/ListCreateView.swift b/Packages/Lists/Sources/Lists/Create/ListCreateView.swift index ad2eed09..f18443d0 100644 --- a/Packages/Lists/Sources/Lists/Create/ListCreateView.swift +++ b/Packages/Lists/Sources/Lists/Create/ListCreateView.swift @@ -24,9 +24,10 @@ public struct ListCreateView: View { Form { Section("lists.edit.settings") { TextField("list.edit.title", text: $title) - Picker("list.edit.repliesPolicy", - selection: $repliesPolicy) - { + Picker( + "list.edit.repliesPolicy", + selection: $repliesPolicy + ) { ForEach(Models.List.RepliesPolicy.allCases) { policy in Text(policy.title) .tag(policy) @@ -45,9 +46,11 @@ public struct ListCreateView: View { let client = client Task { isSaving = true - let _: Models.List = try await client.post(endpoint: Lists.createList(title: title, - repliesPolicy: repliesPolicy, - exclusive: isExclusive)) + let _: Models.List = try await client.post( + endpoint: Lists.createList( + title: title, + repliesPolicy: repliesPolicy, + exclusive: isExclusive)) await currentAccount.fetchLists() isSaving = false dismiss() diff --git a/Packages/Lists/Sources/Lists/Edit/ListEditView.swift b/Packages/Lists/Sources/Lists/Edit/ListEditView.swift index cebeb2a5..972f255a 100644 --- a/Packages/Lists/Sources/Lists/Edit/ListEditView.swift +++ b/Packages/Lists/Sources/Lists/Edit/ListEditView.swift @@ -24,9 +24,10 @@ public struct ListEditView: View { TextField("list.edit.title", text: $viewModel.title) { Task { await viewModel.update() } } - Picker("list.edit.repliesPolicy", - selection: $viewModel.repliesPolicy) - { + Picker( + "list.edit.repliesPolicy", + selection: $viewModel.repliesPolicy + ) { ForEach(Models.List.RepliesPolicy.allCases) { policy in Text(policy.title) .tag(policy) @@ -35,7 +36,7 @@ public struct ListEditView: View { Toggle("list.edit.isExclusive", isOn: $viewModel.isExclusive) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif .disabled(viewModel.isUpdating) .onChange(of: viewModel.repliesPolicy) { _, _ in @@ -47,8 +48,9 @@ public struct ListEditView: View { Section("lists.edit.users-in-list") { HStack { - TextField("lists.edit.users-search", - text: $viewModel.searchUserQuery) + TextField( + "lists.edit.users-search", + text: $viewModel.searchUserQuery) if !viewModel.searchUserQuery.isEmpty { Button { viewModel.searchUserQuery = "" @@ -65,14 +67,14 @@ public struct ListEditView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif .disabled(viewModel.isUpdating) } #if !os(visionOS) - .scrollDismissesKeyboard(.immediately) - .scrollContentBackground(.hidden) - .background(theme.secondaryBackgroundColor) + .scrollDismissesKeyboard(.immediately) + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) #endif .toolbar { ToolbarItem { @@ -126,10 +128,12 @@ public struct ListEditView: View { HStack { AvatarView(account.avatar) VStack(alignment: .leading) { - EmojiTextApp(.init(stringValue: account.safeDisplayName), - emojis: account.emojis) - .emojiText.size(Font.scaledBodyFont.emojiSize) - .emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset) + EmojiTextApp( + .init(stringValue: account.safeDisplayName), + emojis: account.emojis + ) + .emojiText.size(Font.scaledBodyFont.emojiSize) + .emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset) Text("@\(account.acct)") .foregroundStyle(.secondary) .font(.scaledFootnote) @@ -138,25 +142,31 @@ public struct ListEditView: View { Spacer() if let relationship = viewModel.searchedRelationships[account.id] { if relationship.following { - Toggle("", isOn: .init(get: { - viewModel.accounts.contains(where: { $0.id == account.id }) - }, set: { addedToList in - Task { - if addedToList { - await viewModel.add(account: account) - } else { - await viewModel.delete(account: account) - } - } - })) + Toggle( + "", + isOn: .init( + get: { + viewModel.accounts.contains(where: { $0.id == account.id }) + }, + set: { addedToList in + Task { + if addedToList { + await viewModel.add(account: account) + } else { + await viewModel.delete(account: account) + } + } + })) } else { - FollowButton(viewModel: .init(client: client, - accountId: account.id, - relationship: relationship, - shouldDisplayNotify: false, - relationshipUpdated: { relationship in - viewModel.searchedRelationships[account.id] = relationship - })) + FollowButton( + viewModel: .init( + client: client, + accountId: account.id, + relationship: relationship, + shouldDisplayNotify: false, + relationshipUpdated: { relationship in + viewModel.searchedRelationships[account.id] = relationship + })) } } } @@ -173,10 +183,12 @@ public struct ListEditView: View { HStack { AvatarView(account.avatar) VStack(alignment: .leading) { - EmojiTextApp(.init(stringValue: account.safeDisplayName), - emojis: account.emojis) - .emojiText.size(Font.scaledBodyFont.emojiSize) - .emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset) + EmojiTextApp( + .init(stringValue: account.safeDisplayName), + emojis: account.emojis + ) + .emojiText.size(Font.scaledBodyFont.emojiSize) + .emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset) Text("@\(account.acct)") .foregroundStyle(.secondary) .font(.scaledFootnote) diff --git a/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift b/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift index a1f8a634..d827e806 100644 --- a/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift +++ b/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift @@ -47,11 +47,13 @@ import SwiftUI guard let client else { return } do { isUpdating = true - let list: Models.List = try await client.put(endpoint: - Lists.updateList(id: list.id, - title: title, - repliesPolicy: repliesPolicy, - exclusive: isExclusive)) + let list: Models.List = try await client.put( + endpoint: + Lists.updateList( + id: list.id, + title: title, + repliesPolicy: repliesPolicy, + exclusive: isExclusive)) self.list = list title = list.title repliesPolicy = list.repliesPolicy ?? .list @@ -67,7 +69,8 @@ import SwiftUI guard let client else { return } do { isUpdating = true - let response = try await client.post(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id])) + let response = try await client.post( + endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id])) if response?.statusCode == 200 { accounts.append(account) } @@ -81,7 +84,8 @@ import SwiftUI guard let client else { return } do { isUpdating = true - let response = try await client.delete(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id])) + let response = try await client.delete( + endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id])) if response?.statusCode == 200 { accounts.removeAll(where: { $0.id == account.id }) } @@ -95,11 +99,13 @@ import SwiftUI guard let client, !searchUserQuery.isEmpty else { return } do { isSearching = true - let results: SearchResults = try await client.get(endpoint: Search.search(query: searchUserQuery, - type: nil, - offset: nil, - following: nil), - forceVersion: .v2) + let results: SearchResults = try await client.get( + endpoint: Search.search( + query: searchUserQuery, + type: nil, + offset: nil, + following: nil), + forceVersion: .v2) let relationships: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map(\.id))) searchedRelationships = relationships.reduce(into: [String: Relationship]()) { diff --git a/Packages/MediaUI/Package.swift b/Packages/MediaUI/Package.swift index 12745550..9a4396a2 100644 --- a/Packages/MediaUI/Package.swift +++ b/Packages/MediaUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "MediaUI", targets: ["MediaUI"] - ), + ) ], dependencies: [ .package(name: "Models", path: "../Models"), @@ -28,8 +28,8 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] - ), + ) ] ) diff --git a/Packages/MediaUI/Sources/MediaUI/MediaUIAttachmentVideoView.swift b/Packages/MediaUI/Sources/MediaUI/MediaUIAttachmentVideoView.swift index b023209a..ff593e77 100644 --- a/Packages/MediaUI/Sources/MediaUI/MediaUIAttachmentVideoView.swift +++ b/Packages/MediaUI/Sources/MediaUI/MediaUIAttachmentVideoView.swift @@ -31,9 +31,10 @@ import SwiftUI isPlaying = false } guard let player else { return } - NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, - object: player.currentItem, queue: .main) - { _ in + NotificationCenter.default.addObserver( + forName: .AVPlayerItemDidPlayToEndTime, + object: player.currentItem, queue: .main + ) { _ in Task { @MainActor [weak self] in if autoPlay || self?.forceAutoPlay == true { self?.play() @@ -75,7 +76,8 @@ import SwiftUI } deinit { - NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) + NotificationCenter.default.removeObserver( + self, name: .AVPlayerItemDidPlayToEndTime, object: nil) } } @@ -101,27 +103,30 @@ public struct MediaUIAttachmentVideoView: View { if isCatalystWindow { EmptyView() } else { - HStack { } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .contentShape(Rectangle()) - .onTapGesture { - if !preferences.autoPlayVideo && !viewModel.isPlaying { - viewModel.play() - return + HStack {} + .frame(maxWidth: .infinity, maxHeight: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + if !preferences.autoPlayVideo && !viewModel.isPlaying { + viewModel.play() + return + } + #if targetEnvironment(macCatalyst) + viewModel.pause() + let attachement = MediaAttachment.videoWith(url: viewModel.url) + openWindow( + value: WindowDestinationMedia.mediaViewer( + attachments: [attachement], selectedAttachment: attachement)) + #else + isFullScreen = true + #endif } - #if targetEnvironment(macCatalyst) - viewModel.pause() - let attachement = MediaAttachment.videoWith(url: viewModel.url) - openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement], selectedAttachment: attachement)) - #else - isFullScreen = true - #endif - } } }) .onAppear { - viewModel.preparePlayer(autoPlay: isFullScreen ? true : preferences.autoPlayVideo, - isCompact: isCompact) + viewModel.preparePlayer( + autoPlay: isFullScreen ? true : preferences.autoPlayVideo, + isCompact: isCompact) viewModel.mute(preferences.muteVideo) } .onDisappear { @@ -144,13 +149,15 @@ public struct MediaUIAttachmentVideoView: View { } } } - + private var modalPreview: some View { NavigationStack { videoView .toolbar { ToolbarItem(placement: .topBarLeading) { - Button { isFullScreen.toggle() } label: { + Button { + isFullScreen.toggle() + } label: { Image(systemName: "xmark.circle") } } @@ -188,25 +195,30 @@ public struct MediaUIAttachmentVideoView: View { } private var videoView: some View { - VideoPlayer(player: viewModel.player, videoOverlay: { - if !preferences.autoPlayVideo, - !viewModel.forceAutoPlay, - !isFullScreen, - !viewModel.isPlaying, - !isCompact - { - Button(action: { - viewModel.play() - }, label: { - Image(systemName: "play.fill") - .font(isCompact ? .body : .largeTitle) - .foregroundColor(theme.tintColor) - .padding(.all, isCompact ? 6 : nil) - .background(Circle().fill(.thinMaterial)) - .padding(theme.statusDisplayStyle == .compact ? 0 : 10) - }) + VideoPlayer( + player: viewModel.player, + videoOverlay: { + if !preferences.autoPlayVideo, + !viewModel.forceAutoPlay, + !isFullScreen, + !viewModel.isPlaying, + !isCompact + { + Button( + action: { + viewModel.play() + }, + label: { + Image(systemName: "play.fill") + .font(isCompact ? .body : .largeTitle) + .foregroundColor(theme.tintColor) + .padding(.all, isCompact ? 6 : nil) + .background(Circle().fill(.thinMaterial)) + .padding(theme.statusDisplayStyle == .compact ? 0 : 10) + }) + } } - }) + ) .accessibilityAddTraits(.startsMediaSession) .ignoresSafeArea() } diff --git a/Packages/MediaUI/Sources/MediaUI/MediaUIShareLink.swift b/Packages/MediaUI/Sources/MediaUI/MediaUIShareLink.swift index 848502c4..a1826a4b 100644 --- a/Packages/MediaUI/Sources/MediaUI/MediaUIShareLink.swift +++ b/Packages/MediaUI/Sources/MediaUI/MediaUIShareLink.swift @@ -12,8 +12,11 @@ public struct MediaUIShareLink: View, @unchecked Sendable { public var body: some View { if type == .image { let transferable = MediaUIImageTransferable(url: url) - ShareLink(item: transferable, preview: .init("status.media.contextmenu.share", - image: transferable)) + ShareLink( + item: transferable, + preview: .init( + "status.media.contextmenu.share", + image: transferable)) } else { ShareLink(item: url) } diff --git a/Packages/MediaUI/Sources/MediaUI/MediaUIView.swift b/Packages/MediaUI/Sources/MediaUI/MediaUIView.swift index a8c096d8..da01f10a 100644 --- a/Packages/MediaUI/Sources/MediaUI/MediaUIView.swift +++ b/Packages/MediaUI/Sources/MediaUI/MediaUIView.swift @@ -25,14 +25,20 @@ public struct MediaUIView: View, @unchecked Sendable { .focusable() .focused($isFocused) .focusEffectDisabled() - .onKeyPress(.leftArrow, action: { - scrollToPrevious() - return .handled - }) - .onKeyPress(.rightArrow, action: { - scrollToNext() - return .handled - }) + .onKeyPress( + .leftArrow, + action: { + scrollToPrevious() + return .handled + } + ) + .onKeyPress( + .rightArrow, + action: { + scrollToNext() + return .handled + } + ) .scrollTargetBehavior(.viewAligned) .scrollPosition(id: $scrolledItem) .toolbar { @@ -75,9 +81,9 @@ private struct MediaToolBar: ToolbarContent { let data: DisplayData var body: some ToolbarContent { -#if !targetEnvironment(macCatalyst) - DismissToolbarItem() -#endif + #if !targetEnvironment(macCatalyst) + DismissToolbarItem() + #endif QuickLookToolbarItem(itemUrl: data.url) AltTextToolbarItem(alt: data.description) SavePhotoToolbarItem(url: data.url, type: data.type) @@ -112,9 +118,10 @@ private struct AltTextToolbarItem: ToolbarContent { } label: { Text("status.image.alt-text.abbreviation") } - .alert("status.editor.media.image-description", - isPresented: $isAlertDisplayed) - { + .alert( + "status.editor.media.image-description", + isPresented: $isAlertDisplayed + ) { Button("alert.button.ok", action: {}) } message: { Text(alt) diff --git a/Packages/MediaUI/Sources/MediaUI/MediaUIZoomableContainer.swift b/Packages/MediaUI/Sources/MediaUI/MediaUIZoomableContainer.swift index 5ed46dbb..fafec911 100644 --- a/Packages/MediaUI/Sources/MediaUI/MediaUIZoomableContainer.swift +++ b/Packages/MediaUI/Sources/MediaUI/MediaUIZoomableContainer.swift @@ -32,7 +32,10 @@ struct MediaUIZoomableContainer: View { @Binding private var currentScale: CGFloat @Binding private var tapLocation: CGPoint - init(scale: Binding, tapLocation: Binding, @ViewBuilder content: () -> ScollContent) { + init( + scale: Binding, tapLocation: Binding, + @ViewBuilder content: () -> ScollContent + ) { _currentScale = scale _tapLocation = tapLocation self.content = content() @@ -67,15 +70,19 @@ struct MediaUIZoomableContainer: View { func updateUIView(_ uiView: UIScrollView, context: Context) { context.coordinator.hostingController.rootView = content - if uiView.zoomScale > uiView.minimumZoomScale { // Scale out + if uiView.zoomScale > uiView.minimumZoomScale { // Scale out uiView.setZoomScale(currentScale, animated: true) - } else if tapLocation != .zero { // Scale in to a specific point - uiView.zoom(to: zoomRect(for: uiView, scale: uiView.maximumZoomScale, center: tapLocation), animated: true) + } else if tapLocation != .zero { // Scale in to a specific point + uiView.zoom( + to: zoomRect(for: uiView, scale: uiView.maximumZoomScale, center: tapLocation), + animated: true) DispatchQueue.main.async { tapLocation = .zero } } } - @MainActor func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect { + @MainActor func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) + -> CGRect + { let scrollViewSize = scrollView.bounds.size let width = scrollViewSize.width / scale diff --git a/Packages/MediaUI/Sources/MediaUI/QuickLookToolbarItem.swift b/Packages/MediaUI/Sources/MediaUI/QuickLookToolbarItem.swift index d34b95f4..2d2eb87d 100644 --- a/Packages/MediaUI/Sources/MediaUI/QuickLookToolbarItem.swift +++ b/Packages/MediaUI/Sources/MediaUI/QuickLookToolbarItem.swift @@ -44,10 +44,12 @@ struct QuickLookToolbarItem: ToolbarContent, @unchecked Sendable { } private var quickLookDir: URL { - try! FileManager.default.url(for: .cachesDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: false) - .appending(component: "quicklook") + try! FileManager.default.url( + for: .cachesDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false + ) + .appending(component: "quicklook") } } diff --git a/Packages/Models/Package.swift b/Packages/Models/Package.swift index 0a95e5da..6c4f5f1b 100644 --- a/Packages/Models/Package.swift +++ b/Packages/Models/Package.swift @@ -14,19 +14,19 @@ let package = Package( .library( name: "Models", targets: ["Models"] - ), + ) ], dependencies: [ - .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.4.3"), + .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.4.3") ], targets: [ .target( name: "Models", dependencies: [ - "SwiftSoup", + "SwiftSoup" ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] ), .testTarget( diff --git a/Packages/Models/Sources/Models/Account.swift b/Packages/Models/Sources/Models/Account.swift index 73d8c3cf..1f4b9176 100644 --- a/Packages/Models/Sources/Models/Account.swift +++ b/Packages/Models/Sources/Models/Account.swift @@ -2,21 +2,13 @@ import Foundation public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable { public static func == (lhs: Account, rhs: Account) -> Bool { - lhs.id == rhs.id && - lhs.username == rhs.username && - lhs.note.asRawText == rhs.note.asRawText && - lhs.statusesCount == rhs.statusesCount && - lhs.followersCount == rhs.followersCount && - lhs.followingCount == rhs.followingCount && - lhs.acct == rhs.acct && - lhs.displayName == rhs.displayName && - lhs.fields == rhs.fields && - lhs.lastStatusAt == rhs.lastStatusAt && - lhs.discoverable == rhs.discoverable && - lhs.bot == rhs.bot && - lhs.locked == rhs.locked && - lhs.avatar == rhs.avatar && - lhs.header == rhs.header + lhs.id == rhs.id && lhs.username == rhs.username && lhs.note.asRawText == rhs.note.asRawText + && lhs.statusesCount == rhs.statusesCount && lhs.followersCount == rhs.followersCount + && lhs.followingCount == rhs.followingCount && lhs.acct == rhs.acct + && lhs.displayName == rhs.displayName && lhs.fields == rhs.fields + && lhs.lastStatusAt == rhs.lastStatusAt && lhs.discoverable == rhs.discoverable + && lhs.bot == rhs.bot && lhs.locked == rhs.locked && lhs.avatar == rhs.avatar + && lhs.header == rhs.header } public func hash(into hasher: inout Hasher) { @@ -75,7 +67,13 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable "\(acct)@\(url?.host() ?? "")" } - public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil, moved: Account? = nil) { + public init( + id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, + note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, + statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, + emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, + discoverable: Bool? = nil, moved: Account? = nil + ) { self.id = id self.username = username self.displayName = displayName @@ -158,30 +156,39 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable } public static func placeholder() -> Account { - .init(id: UUID().uuidString, - username: "Username", - displayName: "John Mastodon", - avatar: URL(string: "https://files.mastodon.social/media_attachments/files/003/134/405/original/04060b07ddf7bb0b.png")!, - header: URL(string: "https://files.mastodon.social/media_attachments/files/003/134/405/original/04060b07ddf7bb0b.png")!, - acct: "johnm@example.com", - note: .init(stringValue: "Some content"), - createdAt: ServerDate(), - followersCount: 10, - followingCount: 10, - statusesCount: 10, - lastStatusAt: nil, - fields: [], - locked: false, - emojis: [], - url: nil, - source: nil, - bot: false, - discoverable: true) + .init( + id: UUID().uuidString, + username: "Username", + displayName: "John Mastodon", + avatar: URL( + string: + "https://files.mastodon.social/media_attachments/files/003/134/405/original/04060b07ddf7bb0b.png" + )!, + header: URL( + string: + "https://files.mastodon.social/media_attachments/files/003/134/405/original/04060b07ddf7bb0b.png" + )!, + acct: "johnm@example.com", + note: .init(stringValue: "Some content"), + createdAt: ServerDate(), + followersCount: 10, + followingCount: 10, + statusesCount: 10, + lastStatusAt: nil, + fields: [], + locked: false, + emojis: [], + url: nil, + source: nil, + bot: false, + discoverable: true) } public static func placeholders() -> [Account] { - [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), - .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + [ + .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), + .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), + ] } } @@ -200,27 +207,32 @@ extension Account { } return fields.first(where: { $0.value.asRawText.contains(AppInfo.premiumInstance) }) != nil } - + public var premiumAcct: String? { if isPremiumAccount { return "@\(acct)" - } else if let field = fields.first(where: { $0.value.asRawText.hasSuffix(AppInfo.premiumInstance) }) { + } else if let field = fields.first(where: { + $0.value.asRawText.hasSuffix(AppInfo.premiumInstance) + }) { return field.value.asRawText - } else if let field = fields.first(where: { $0.value.asRawText.hasPrefix("https://\(AppInfo.premiumInstance)") }), - let url = URL(string: field.value.asRawText) { + } else if let field = fields.first(where: { + $0.value.asRawText.hasPrefix("https://\(AppInfo.premiumInstance)") + }), + let url = URL(string: field.value.asRawText) + { return "\(url.lastPathComponent)@\(url.host() ?? "\(AppInfo.premiumInstance)")" } return nil } - + public var premiumUsername: String? { var username = premiumAcct?.replacingOccurrences(of: "@\(AppInfo.premiumInstance)", with: "") username?.removeFirst() return username } - + public var isPremiumAccount: Bool { url?.host() == AppInfo.premiumInstance } - + } diff --git a/Packages/Models/Sources/Models/Alias/HTMLString.swift b/Packages/Models/Sources/Models/Alias/HTMLString.swift index fa0de55b..e11ff0d7 100644 --- a/Packages/Models/Sources/Models/Alias/HTMLString.swift +++ b/Packages/Models/Sources/Models/Alias/HTMLString.swift @@ -42,9 +42,11 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { // to attributed text. Note that ~ for strikethrough is // not documented in the syntax docs but is used by // AttributedString. - main_regex = try? NSRegularExpression(pattern: "([\\*\\`\\~\\[\\\\])", options: .caseInsensitive) + main_regex = try? NSRegularExpression( + pattern: "([\\*\\`\\~\\[\\\\])", options: .caseInsensitive) // don't escape underscores that are between colons, they are most likely custom emoji - underscore_regex = try? NSRegularExpression(pattern: "(?!\\B:[^:]*)(_)(?![^:]*:\\B)", options: .caseInsensitive) + underscore_regex = try? NSRegularExpression( + pattern: "(?!\\B:[^:]*)(_)(?![^:]*:\\B)", options: .caseInsensitive) asMarkdown = "" do { @@ -56,7 +58,9 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { try document.select("br").after("\n") try document.select("p").after("\n\n") let html = try document.html() - var text = try SwiftSoup.clean(html, "", Whitelist.none(), OutputSettings().prettyPrint(pretty: false)) ?? "" + var text = + try SwiftSoup.clean( + html, "", Whitelist.none(), OutputSettings().prettyPrint(pretty: false)) ?? "" // Remove the two last line break added after the last paragraph. if text.hasSuffix("\n\n") { _ = text.removeLast() @@ -74,8 +78,9 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { } do { - let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true, - interpretedSyntax: .inlineOnlyPreservingWhitespace) + let options = AttributedString.MarkdownParsingOptions( + allowsExtendedAttributes: true, + interpretedSyntax: .inlineOnlyPreservingWhitespace) asSafeMarkdownAttributedString = try AttributedString(markdown: asMarkdown, options: options) } catch { asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue) @@ -90,9 +95,11 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { if parseMarkdown { do { - let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true, - interpretedSyntax: .inlineOnlyPreservingWhitespace) - asSafeMarkdownAttributedString = try AttributedString(markdown: asMarkdown, options: options) + let options = AttributedString.MarkdownParsingOptions( + allowsExtendedAttributes: true, + interpretedSyntax: .inlineOnlyPreservingWhitespace) + asSafeMarkdownAttributedString = try AttributedString( + markdown: asMarkdown, options: options) } catch { asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue) } @@ -110,10 +117,12 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { try container.encode(links, forKey: .links) } - private mutating func handleNode(node: SwiftSoup.Node, - indent: Int? = 0, - skipParagraph: Bool = false, - listCounters: inout [Int]) { + private mutating func handleNode( + node: SwiftSoup.Node, + indent: Int? = 0, + skipParagraph: Bool = false, + listCounters: inout [Int] + ) { do { if let className = try? node.attr("class") { if className == "invisible" { @@ -137,7 +146,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { asMarkdown += "\n\n" } } else if node.nodeName() == "br" { - if asMarkdown.count > 0 { // ignore first opening
+ if asMarkdown.count > 0 { // ignore first opening
asMarkdown += "\n" } if (indent ?? 0) > 0 { @@ -150,8 +159,9 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { if Int(url.lastPathComponent) != nil { statusesURLs.append(url) } else if url.host() == "www.threads.net" || url.host() == "threads.net", - url.pathComponents.count == 4, - url.pathComponents[2] == "post" { + url.pathComponents.count == 4, + url.pathComponents[2] == "post" + { statusesURLs.append(url) } } @@ -175,7 +185,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { } if let linkUrl = url { linkRef = linkUrl.absoluteString - let displayString = asMarkdown[start ..< finish] + let displayString = asMarkdown[start..s - asMarkdown += txt.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: "\u{2028}", with: "") + asMarkdown += txt.replacingOccurrences(of: "\n", with: "").replacingOccurrences( + of: "\u{2028}", with: "") } else if node.nodeName() == "blockquote" { asMarkdown += "\n\n`" for nn in node.getChildNodes() { @@ -218,27 +233,27 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { asMarkdown += "_" return } else if node.nodeName() == "ul" || node.nodeName() == "ol" { - + if skipParagraph { asMarkdown += "\n" } else { asMarkdown += "\n\n" } - + var listCounters = listCounters - + if node.nodeName() == "ol" { - listCounters.append(1) // Start numbering for a new ordered list + listCounters.append(1) // Start numbering for a new ordered list } - + for nn in node.getChildNodes() { handleNode(node: nn, indent: (indent ?? 0) + 1, listCounters: &listCounters) } - + if node.nodeName() == "ol" { listCounters.removeLast() } - + return } else if node.nodeName() == "li" { asMarkdown += " " @@ -248,8 +263,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { } asMarkdown += "- " } - - + if listCounters.isEmpty { asMarkdown += "• " } else { @@ -257,7 +271,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { asMarkdown += "\(listCounters[currentIndex]). " listCounters[currentIndex] += 1 } - + for nn in node.getChildNodes() { handleNode(node: nn, indent: indent, skipParagraph: true, listCounters: &listCounters) } @@ -307,18 +321,18 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { } } -public extension URL { +extension URL { // It's common to use non-ASCII characters in URLs even though they're technically // invalid characters. Every modern browser handles this by silently encoding // the invalid characters on the user's behalf. However, trying to create a URL // object with un-encoded characters will result in nil so we need to encode the // invalid characters before creating the URL object. The unencoded version // should still be shown in the displayed status. - init?(string: String, encodePath: Bool) { + public init?(string: String, encodePath: Bool) { var encodedUrlString = "" if encodePath, - string.starts(with: "http://") || string.starts(with: "https://"), - var startIndex = string.firstIndex(of: "/") + string.starts(with: "http://") || string.starts(with: "https://"), + var startIndex = string.firstIndex(of: "/") { startIndex = string.index(startIndex, offsetBy: 1) @@ -327,17 +341,29 @@ public extension URL { encodedUrlString = String(string[...startIndex]) while let endIndex = string[string.index(after: startIndex)...].firstIndex(of: "/") { let componentStartIndex = string.index(after: startIndex) - encodedUrlString = encodedUrlString + (string[componentStartIndex ... endIndex].addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "") + encodedUrlString = + encodedUrlString + + (string[componentStartIndex...endIndex].addingPercentEncoding( + withAllowedCharacters: .urlPathAllowed) ?? "") startIndex = endIndex } // The last part of the path may have a query string appended to it let componentStartIndex = string.index(after: startIndex) if let queryStartIndex = string[componentStartIndex...].firstIndex(of: "?") { - encodedUrlString = encodedUrlString + (string[componentStartIndex ..< queryStartIndex].addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "") - encodedUrlString = encodedUrlString + (string[queryStartIndex...].addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") + encodedUrlString = + encodedUrlString + + (string[componentStartIndex.. ConsolidatedNotification { - .init(notifications: [Notification.placeholder()], - type: .favourite, - createdAt: ServerDate(), - accounts: [.placeholder()], - status: .placeholder()) + .init( + notifications: [Notification.placeholder()], + type: .favourite, + createdAt: ServerDate(), + accounts: [.placeholder()], + status: .placeholder()) } public static func placeholders() -> [ConsolidatedNotification] { - [.placeholder(), .placeholder(), .placeholder(), - .placeholder(), .placeholder(), .placeholder(), - .placeholder(), .placeholder(), .placeholder(), - .placeholder(), .placeholder(), .placeholder()] + [ + .placeholder(), .placeholder(), .placeholder(), + .placeholder(), .placeholder(), .placeholder(), + .placeholder(), .placeholder(), .placeholder(), + .placeholder(), .placeholder(), .placeholder(), + ] } } diff --git a/Packages/Models/Sources/Models/Conversation.swift b/Packages/Models/Sources/Models/Conversation.swift index 7935cf88..f9d0652c 100644 --- a/Packages/Models/Sources/Models/Conversation.swift +++ b/Packages/Models/Sources/Models/Conversation.swift @@ -14,12 +14,15 @@ public struct Conversation: Identifiable, Decodable, Hashable, Equatable { } public static func placeholder() -> Conversation { - .init(id: UUID().uuidString, unread: false, lastStatus: .placeholder(), accounts: [.placeholder()]) + .init( + id: UUID().uuidString, unread: false, lastStatus: .placeholder(), accounts: [.placeholder()]) } public static func placeholders() -> [Conversation] { - [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), - .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + [ + .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), + .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), + ] } } diff --git a/Packages/Models/Sources/Models/Language.swift b/Packages/Models/Sources/Models/Language.swift index 667b266c..77a51160 100644 --- a/Packages/Models/Sources/Models/Language.swift +++ b/Packages/Models/Sources/Models/Language.swift @@ -15,7 +15,8 @@ public struct Language: Identifiable, Equatable, Hashable { return Language( isoCode: lang.identifier, nativeName: nativeLocale.localizedString(forLanguageCode: lang.identifier)?.capitalized, - localizedName: Locale.current.localizedString(forLanguageCode: lang.identifier)?.localizedCapitalized + localizedName: Locale.current.localizedString(forLanguageCode: lang.identifier)? + .localizedCapitalized ) } } diff --git a/Packages/Models/Sources/Models/List.swift b/Packages/Models/Sources/Models/List.swift index 4891dec8..83672f02 100644 --- a/Packages/Models/Sources/Models/List.swift +++ b/Packages/Models/Sources/Models/List.swift @@ -14,7 +14,9 @@ public struct List: Codable, Identifiable, Equatable, Hashable { case followed, list, none } - public init(id: String, title: String, repliesPolicy: RepliesPolicy? = nil, exclusive: Bool? = nil) { + public init( + id: String, title: String, repliesPolicy: RepliesPolicy? = nil, exclusive: Bool? = nil + ) { self.id = id self.title = title self.repliesPolicy = repliesPolicy diff --git a/Packages/Models/Sources/Models/MediaAttachement.swift b/Packages/Models/Sources/Models/MediaAttachement.swift index 82f098b9..4afbc12a 100644 --- a/Packages/Models/Sources/Models/MediaAttachement.swift +++ b/Packages/Models/Sources/Models/MediaAttachement.swift @@ -28,13 +28,21 @@ public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable { if let supportedType { switch supportedType { case .image: - return NSLocalizedString("accessibility.media.supported-type.image.label", bundle: .main, comment: "A localized description of SupportedType.image") + return NSLocalizedString( + "accessibility.media.supported-type.image.label", bundle: .main, + comment: "A localized description of SupportedType.image") case .gifv: - return NSLocalizedString("accessibility.media.supported-type.gifv.label", bundle: .main, comment: "A localized description of SupportedType.gifv") + return NSLocalizedString( + "accessibility.media.supported-type.gifv.label", bundle: .main, + comment: "A localized description of SupportedType.gifv") case .video: - return NSLocalizedString("accessibility.media.supported-type.video.label", bundle: .main, comment: "A localized description of SupportedType.video") + return NSLocalizedString( + "accessibility.media.supported-type.video.label", bundle: .main, + comment: "A localized description of SupportedType.video") case .audio: - return NSLocalizedString("accessibility.media.supported-type.audio.label", bundle: .main, comment: "A localized description of SupportedType.audio") + return NSLocalizedString( + "accessibility.media.supported-type.audio.label", bundle: .main, + comment: "A localized description of SupportedType.audio") } } return nil @@ -46,21 +54,23 @@ public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable { public let meta: MetaContainer? public static func imageWith(url: URL) -> MediaAttachment { - .init(id: UUID().uuidString, - type: "image", - url: url, - previewUrl: url, - description: nil, - meta: nil) + .init( + id: UUID().uuidString, + type: "image", + url: url, + previewUrl: url, + description: nil, + meta: nil) } public static func videoWith(url: URL) -> MediaAttachment { - .init(id: UUID().uuidString, - type: "video", - url: url, - previewUrl: url, - description: nil, - meta: nil) + .init( + id: UUID().uuidString, + type: "video", + url: url, + previewUrl: url, + description: nil, + meta: nil) } } diff --git a/Packages/Models/Sources/Models/Notification.swift b/Packages/Models/Sources/Models/Notification.swift index 405a2de5..ad324ade 100644 --- a/Packages/Models/Sources/Models/Notification.swift +++ b/Packages/Models/Sources/Models/Notification.swift @@ -16,11 +16,12 @@ public struct Notification: Decodable, Identifiable, Equatable { } public static func placeholder() -> Notification { - .init(id: UUID().uuidString, - type: NotificationType.favourite.rawValue, - createdAt: ServerDate(), - account: .placeholder(), - status: .placeholder()) + .init( + id: UUID().uuidString, + type: NotificationType.favourite.rawValue, + createdAt: ServerDate(), + account: .placeholder(), + status: .placeholder()) } } diff --git a/Packages/Models/Sources/Models/NotificationsPolicy.swift b/Packages/Models/Sources/Models/NotificationsPolicy.swift index e4990cb9..f8692388 100644 --- a/Packages/Models/Sources/Models/NotificationsPolicy.swift +++ b/Packages/Models/Sources/Models/NotificationsPolicy.swift @@ -4,7 +4,7 @@ public struct NotificationsPolicy: Codable, Sendable { public enum Policy: String, Codable, Sendable, CaseIterable, Hashable { case accept, filter, drop } - + public var forNotFollowing: Policy public var forNotFollowers: Policy public var forNewAccounts: Policy diff --git a/Packages/Models/Sources/Models/PostError.swift b/Packages/Models/Sources/Models/PostError.swift index 4f036f73..81d8db1b 100644 --- a/Packages/Models/Sources/Models/PostError.swift +++ b/Packages/Models/Sources/Models/PostError.swift @@ -9,7 +9,8 @@ extension PostError: CustomStringConvertible { public var description: String { switch self { case .missingAltText: - return NSLocalizedString("status.error.no-alt-text", comment: "media does not have media description") + return NSLocalizedString( + "status.error.no-alt-text", comment: "media does not have media description") } } } diff --git a/Packages/Models/Sources/Models/Relationship.swift b/Packages/Models/Sources/Models/Relationship.swift index 04359fc5..461e0ea4 100644 --- a/Packages/Models/Sources/Models/Relationship.swift +++ b/Packages/Models/Sources/Models/Relationship.swift @@ -16,24 +16,25 @@ public struct Relationship: Codable, Equatable, Identifiable { public let notifying: Bool public static func placeholder() -> Relationship { - .init(id: UUID().uuidString, - following: false, - showingReblogs: false, - followedBy: false, - blocking: false, - blockedBy: false, - muting: false, - mutingNotifications: false, - requested: false, - domainBlocking: false, - endorsed: false, - note: "", - notifying: false) + .init( + id: UUID().uuidString, + following: false, + showingReblogs: false, + followedBy: false, + blocking: false, + blockedBy: false, + muting: false, + mutingNotifications: false, + requested: false, + domainBlocking: false, + endorsed: false, + note: "", + notifying: false) } } -public extension Relationship { - init(from decoder: Decoder) throws { +extension Relationship { + public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) id = try values.decodeIfPresent(String.self, forKey: .id) ?? "" following = try values.decodeIfPresent(Bool.self, forKey: .following) ?? false @@ -42,7 +43,8 @@ public extension Relationship { blocking = try values.decodeIfPresent(Bool.self, forKey: .blocking) ?? false blockedBy = try values.decodeIfPresent(Bool.self, forKey: .blockedBy) ?? false muting = try values.decodeIfPresent(Bool.self, forKey: .muting) ?? false - mutingNotifications = try values.decodeIfPresent(Bool.self, forKey: .mutingNotifications) ?? false + mutingNotifications = + try values.decodeIfPresent(Bool.self, forKey: .mutingNotifications) ?? false requested = try values.decodeIfPresent(Bool.self, forKey: .requested) ?? false domainBlocking = try values.decodeIfPresent(Bool.self, forKey: .domainBlocking) ?? false endorsed = try values.decodeIfPresent(Bool.self, forKey: .endorsed) ?? false diff --git a/Packages/Models/Sources/Models/ServerFilter.swift b/Packages/Models/Sources/Models/ServerFilter.swift index 0ca949cc..e4c7607c 100644 --- a/Packages/Models/Sources/Models/ServerFilter.swift +++ b/Packages/Models/Sources/Models/ServerFilter.swift @@ -36,8 +36,8 @@ public struct ServerFilter: Codable, Identifiable, Hashable, Sendable { } } -public extension ServerFilter.Context { - var iconName: String { +extension ServerFilter.Context { + public var iconName: String { switch self { case .home: "rectangle.stack" @@ -52,7 +52,7 @@ public extension ServerFilter.Context { } } - var name: String { + public var name: String { switch self { case .home: NSLocalizedString("filter.contexts.home", comment: "") @@ -68,8 +68,8 @@ public extension ServerFilter.Context { } } -public extension ServerFilter.Action { - var label: String { +extension ServerFilter.Action { + public var label: String { switch self { case .warn: NSLocalizedString("filter.action.warning", comment: "") diff --git a/Packages/Models/Sources/Models/Status.swift b/Packages/Models/Sources/Models/Status.swift index e24bfdf3..ec963b38 100644 --- a/Packages/Models/Sources/Models/Status.swift +++ b/Packages/Models/Sources/Models/Status.swift @@ -82,7 +82,15 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable mediaAttachments.map { .init(status: self, attachment: $0) } } - public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) { + public init( + id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, + reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], + repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, + reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, + application: Application?, inReplyToId: String?, inReplyToAccountId: String?, + visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, + sensitive: Bool, language: String? + ) { self.id = id self.content = content self.account = account @@ -113,71 +121,77 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable } public static func placeholder(forSettings: Bool = false, language: String? = nil) -> Status { - .init(id: UUID().uuidString, - content: .init(stringValue: "Here's to the [#crazy](#) ones. The misfits.\nThe [@rebels](#). The troublemakers.", - parseMarkdown: forSettings), + .init( + id: UUID().uuidString, + content: .init( + stringValue: + "Here's to the [#crazy](#) ones. The misfits.\nThe [@rebels](#). The troublemakers.", + parseMarkdown: forSettings), - account: .placeholder(), - createdAt: ServerDate(), - editedAt: nil, - reblog: nil, - mediaAttachments: [], - mentions: [], - repliesCount: 34, - reblogsCount: 8, - favouritesCount: 150, - card: nil, - favourited: false, - reblogged: false, - pinned: false, - bookmarked: false, - emojis: [], - url: "https://example.com", - application: nil, - inReplyToId: nil, - inReplyToAccountId: nil, - visibility: .pub, - poll: nil, - spoilerText: .init(stringValue: ""), - filtered: [], - sensitive: false, - language: language) + account: .placeholder(), + createdAt: ServerDate(), + editedAt: nil, + reblog: nil, + mediaAttachments: [], + mentions: [], + repliesCount: 34, + reblogsCount: 8, + favouritesCount: 150, + card: nil, + favourited: false, + reblogged: false, + pinned: false, + bookmarked: false, + emojis: [], + url: "https://example.com", + application: nil, + inReplyToId: nil, + inReplyToAccountId: nil, + visibility: .pub, + poll: nil, + spoilerText: .init(stringValue: ""), + filtered: [], + sensitive: false, + language: language) } public static func placeholders() -> [Status] { - [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), - .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + [ + .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), + .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), + ] } public var reblogAsAsStatus: Status? { if let reblog { - return .init(id: reblog.id, - content: reblog.content, - account: reblog.account, - createdAt: reblog.createdAt, - editedAt: reblog.editedAt, - reblog: nil, - mediaAttachments: reblog.mediaAttachments, - mentions: reblog.mentions, - repliesCount: reblog.repliesCount, - reblogsCount: reblog.reblogsCount, - favouritesCount: reblog.favouritesCount, - card: reblog.card, - favourited: reblog.favourited, - reblogged: reblog.reblogged, - pinned: reblog.pinned, - bookmarked: reblog.bookmarked, - emojis: reblog.emojis, - url: reblog.url, - application: reblog.application, - inReplyToId: reblog.inReplyToId, - inReplyToAccountId: reblog.inReplyToAccountId, - visibility: reblog.visibility, - poll: reblog.poll, - spoilerText: reblog.spoilerText, - filtered: reblog.filtered, - sensitive: reblog.sensitive, - language: reblog.language) + return .init( + id: reblog.id, + content: reblog.content, + account: reblog.account, + createdAt: reblog.createdAt, + editedAt: reblog.editedAt, + reblog: nil, + mediaAttachments: reblog.mediaAttachments, + mentions: reblog.mentions, + repliesCount: reblog.repliesCount, + reblogsCount: reblog.reblogsCount, + favouritesCount: reblog.favouritesCount, + card: reblog.card, + favourited: reblog.favourited, + reblogged: reblog.reblogged, + pinned: reblog.pinned, + bookmarked: reblog.bookmarked, + emojis: reblog.emojis, + url: reblog.url, + application: reblog.application, + inReplyToId: reblog.inReplyToId, + inReplyToAccountId: reblog.inReplyToAccountId, + visibility: reblog.visibility, + poll: reblog.poll, + spoilerText: reblog.spoilerText, + filtered: reblog.filtered, + sensitive: reblog.sensitive, + language: reblog.language) } return nil } @@ -223,7 +237,14 @@ public final class ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Ha filtered?.first?.filter.filterAction == .hide } - public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application? = nil, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) { + public init( + id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, + mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, + favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, + bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application? = nil, + inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, + spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String? + ) { self.id = id self.content = content self.account = account diff --git a/Packages/Models/Sources/Models/Stream/StreamEvent.swift b/Packages/Models/Sources/Models/Stream/StreamEvent.swift index c2a4dd6e..bfcbe836 100644 --- a/Packages/Models/Sources/Models/Stream/StreamEvent.swift +++ b/Packages/Models/Sources/Models/Stream/StreamEvent.swift @@ -1,6 +1,6 @@ import Foundation -public struct RawStreamEvent: Decodable, Sendable{ +public struct RawStreamEvent: Decodable, Sendable { public let event: String public let stream: [String] public let payload: String diff --git a/Packages/Models/Sources/Models/SubClubUser.swift b/Packages/Models/Sources/Models/SubClubUser.swift index db5d142e..b8621d69 100644 --- a/Packages/Models/Sources/Models/SubClubUser.swift +++ b/Packages/Models/Sources/Models/SubClubUser.swift @@ -7,7 +7,7 @@ public struct SubClubUser: Sendable, Identifiable, Decodable { public let interval: String public let intervalCount: Int public let unitAmount: Int - + public var formattedAmount: String { let formatter = NumberFormatter() formatter.numberStyle = .currency @@ -16,7 +16,7 @@ public struct SubClubUser: Sendable, Identifiable, Decodable { return formatter.string(from: .init(integerLiteral: unitAmount / 100)) ?? "$NaN" } } - + public let id: String public let subscription: Subscription? } diff --git a/Packages/Models/Sources/Models/SwiftData/RecentTag.swift b/Packages/Models/Sources/Models/SwiftData/RecentTag.swift index 2b6d8250..6d29f24d 100644 --- a/Packages/Models/Sources/Models/SwiftData/RecentTag.swift +++ b/Packages/Models/Sources/Models/SwiftData/RecentTag.swift @@ -14,6 +14,7 @@ import SwiftUI extension RecentTag { public var formattedDate: String { - DateFormatterCache.shared.createdAtRelativeFormatter.localizedString(for: lastUse, relativeTo: Date()) + DateFormatterCache.shared.createdAtRelativeFormatter.localizedString( + for: lastUse, relativeTo: Date()) } } diff --git a/Packages/Models/Sources/Models/Tag.swift b/Packages/Models/Sources/Models/Tag.swift index 676a027d..00715555 100644 --- a/Packages/Models/Sources/Models/Tag.swift +++ b/Packages/Models/Sources/Models/Tag.swift @@ -6,8 +6,7 @@ public struct Tag: Codable, Identifiable, Equatable, Hashable { } public static func == (lhs: Tag, rhs: Tag) -> Bool { - lhs.name == rhs.name && - lhs.following == rhs.following + lhs.name == rhs.name && lhs.following == rhs.following } public var id: String { diff --git a/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift b/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift index 1bf3ab5b..3461e7c1 100644 --- a/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift +++ b/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift @@ -1,6 +1,7 @@ -@testable import Models -import Testing import Foundation +import Testing + +@testable import Models @Test func testURLInit() throws { @@ -10,15 +11,22 @@ func testURLInit() throws { let urlWithTrailingSlash = URL(string: "https://www.google.com/", encodePath: true) #expect("https://www.google.com/" == urlWithTrailingSlash?.absoluteString) - let extendedCharPath = URL(string: "https://en.wikipedia.org/wiki/Elbbrücken_station", encodePath: true) - #expect("https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station" == extendedCharPath?.absoluteString) + let extendedCharPath = URL( + string: "https://en.wikipedia.org/wiki/Elbbrücken_station", encodePath: true) + #expect( + "https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station" == extendedCharPath?.absoluteString) let extendedCharQuery = URL(string: "http://test.com/blah/city?name=京都市", encodePath: true) - #expect("http://test.com/blah/city?name=%E4%BA%AC%E9%83%BD%E5%B8%82" == extendedCharQuery?.absoluteString) + #expect( + "http://test.com/blah/city?name=%E4%BA%AC%E9%83%BD%E5%B8%82" + == extendedCharQuery?.absoluteString) // Double encoding will happen if you ask to encodePath on an already encoded string - let alreadyEncodedPath = URL(string: "https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", encodePath: true) - #expect("https://en.wikipedia.org/wiki/Elbbr%25C3%25BCcken_station" == alreadyEncodedPath?.absoluteString) + let alreadyEncodedPath = URL( + string: "https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", encodePath: true) + #expect( + "https://en.wikipedia.org/wiki/Elbbr%25C3%25BCcken_station" + == alreadyEncodedPath?.absoluteString) } @Test @@ -53,10 +61,13 @@ func testHTMLStringInit() throws { #expect("https://test.com/go%C3%9F%C3%AB%C3%B1a" == htmlString.links[0].url.absoluteString) #expect("test" == htmlString.links[0].displayString) - let alreadyEncodedLink = "\"

This is a test

\"" + let alreadyEncodedLink = + "\"

This is a test

\"" htmlString = try decoder.decode(HTMLString.self, from: Data(alreadyEncodedLink.utf8)) #expect("This is a test" == htmlString.asRawText) - #expect("

This is a test

" == htmlString.htmlValue) + #expect( + "

This is a test

" + == htmlString.htmlValue) #expect("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)" == htmlString.asMarkdown) #expect(0 == htmlString.statusesURLs.count) #expect(1 == htmlString.links.count) diff --git a/Packages/Network/Package.swift b/Packages/Network/Package.swift index b9ccc459..a700b4a8 100644 --- a/Packages/Network/Package.swift +++ b/Packages/Network/Package.swift @@ -14,19 +14,19 @@ let package = Package( .library( name: "Network", targets: ["Network"] - ), + ) ], dependencies: [ - .package(name: "Models", path: "../Models"), + .package(name: "Models", path: "../Models") ], targets: [ .target( name: "Network", dependencies: [ - .product(name: "Models", package: "Models"), + .product(name: "Models", package: "Models") ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] ), .testTarget( diff --git a/Packages/Network/Sources/Network/Client.swift b/Packages/Network/Sources/Network/Client.swift index b926f74b..ab7acf5e 100644 --- a/Packages/Network/Sources/Network/Client.swift +++ b/Packages/Network/Sources/Network/Client.swift @@ -1,19 +1,18 @@ import Combine import Foundation import Models -import Observation -import os import OSLog +import Observation import SwiftUI +import os @Observable public final class Client: Equatable, Identifiable, Hashable, @unchecked Sendable { public static func == (lhs: Client, rhs: Client) -> Bool { let lhsToken = lhs.critical.withLock { $0.oauthToken } let rhsToken = rhs.critical.withLock { $0.oauthToken } - return (lhsToken != nil) == (rhsToken != nil) && - lhs.server == rhs.server && - lhsToken?.accessToken == rhsToken?.accessToken + return (lhsToken != nil) == (rhsToken != nil) && lhs.server == rhs.server + && lhsToken?.accessToken == rhsToken?.accessToken } public enum Version: String, Sendable { @@ -93,11 +92,12 @@ import SwiftUI } } - private func makeURL(scheme: String = "https", - endpoint: Endpoint, - forceVersion: Version? = nil, - forceServer: String? = nil) throws -> URL - { + private func makeURL( + scheme: String = "https", + endpoint: Endpoint, + forceVersion: Version? = nil, + forceServer: String? = nil + ) throws -> URL { var components = URLComponents() components.scheme = scheme components.host = forceServer ?? server @@ -139,16 +139,20 @@ import SwiftUI return makeURLRequest(url: url, endpoint: endpoint, httpMethod: "GET") } - public func get(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity { + public func get(endpoint: Endpoint, forceVersion: Version? = nil) async throws + -> Entity + { try await makeEntityRequest(endpoint: endpoint, method: "GET", forceVersion: forceVersion) } - public func getWithLink(endpoint: Endpoint) async throws -> (Entity, LinkHandler?) { + public func getWithLink(endpoint: Endpoint) async throws -> ( + Entity, LinkHandler? + ) { let request = try makeGet(endpoint: endpoint) let (data, httpResponse) = try await urlSession.data(for: request) var linkHandler: LinkHandler? if let response = httpResponse as? HTTPURLResponse, - let link = response.allHeaderFields["Link"] as? String + let link = response.allHeaderFields["Link"] as? String { linkHandler = .init(rawLink: link) } @@ -157,11 +161,15 @@ import SwiftUI return try (decoder.decode(Entity.self, from: data), linkHandler) } - public func post(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity { + public func post(endpoint: Endpoint, forceVersion: Version? = nil) async throws + -> Entity + { try await makeEntityRequest(endpoint: endpoint, method: "POST", forceVersion: forceVersion) } - public func post(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> HTTPURLResponse? { + public func post(endpoint: Endpoint, forceVersion: Version? = nil) async throws + -> HTTPURLResponse? + { let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion) let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "POST") let (_, httpResponse) = try await urlSession.data(for: request) @@ -175,21 +183,26 @@ import SwiftUI return httpResponse as? HTTPURLResponse } - public func put(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity { + public func put(endpoint: Endpoint, forceVersion: Version? = nil) async throws + -> Entity + { try await makeEntityRequest(endpoint: endpoint, method: "PUT", forceVersion: forceVersion) } - public func delete(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> HTTPURLResponse? { + public func delete(endpoint: Endpoint, forceVersion: Version? = nil) async throws + -> HTTPURLResponse? + { let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion) let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "DELETE") let (_, httpResponse) = try await urlSession.data(for: request) return httpResponse as? HTTPURLResponse } - private func makeEntityRequest(endpoint: Endpoint, - method: String, - forceVersion: Version? = nil) async throws -> Entity - { + private func makeEntityRequest( + endpoint: Endpoint, + method: String, + forceVersion: Version? = nil + ) async throws -> Entity { let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion) let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) let (data, httpResponse) = try await urlSession.data(for: request) @@ -219,19 +232,24 @@ import SwiftUI throw OauthError.missingApp } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let code = components.queryItems?.first(where: { $0.name == "code" })?.value + let code = components.queryItems?.first(where: { $0.name == "code" })?.value else { throw OauthError.invalidRedirectURL } - let token: OauthToken = try await post(endpoint: Oauth.token(code: code, - clientId: app.clientId, - clientSecret: app.clientSecret)) + let token: OauthToken = try await post( + endpoint: Oauth.token( + code: code, + clientId: app.clientId, + clientSecret: app.clientSecret)) critical.withLock { $0.oauthToken = token } return token } - public func makeWebSocketTask(endpoint: Endpoint, instanceStreamingURL: URL?) throws -> URLSessionWebSocketTask { - let url = try makeURL(scheme: "wss", endpoint: endpoint, forceServer: instanceStreamingURL?.host) + public func makeWebSocketTask(endpoint: Endpoint, instanceStreamingURL: URL?) throws + -> URLSessionWebSocketTask + { + let url = try makeURL( + scheme: "wss", endpoint: endpoint, forceServer: instanceStreamingURL?.host) var subprotocols: [String] = [] if let oauthToken = critical.withLock({ $0.oauthToken }) { subprotocols.append(oauthToken.accessToken) @@ -239,19 +257,21 @@ import SwiftUI return urlSession.webSocketTask(with: url, protocols: subprotocols) } - public func mediaUpload(endpoint: Endpoint, - version: Version, - method: String, - mimeType: String, - filename: String, - data: Data) async throws -> Entity - { - let request = try makeFormDataRequest(endpoint: endpoint, - version: version, - method: method, - mimeType: mimeType, - filename: filename, - data: data) + public func mediaUpload( + endpoint: Endpoint, + version: Version, + method: String, + mimeType: String, + filename: String, + data: Data + ) async throws -> Entity { + let request = try makeFormDataRequest( + endpoint: endpoint, + version: version, + method: method, + mimeType: mimeType, + filename: filename, + data: data) let (data, httpResponse) = try await urlSession.data(for: request) logResponseOnError(httpResponse: httpResponse, data: data) do { @@ -264,37 +284,43 @@ import SwiftUI } } - public func mediaUpload(endpoint: Endpoint, - version: Version, - method: String, - mimeType: String, - filename: String, - data: Data) async throws -> HTTPURLResponse? - { - let request = try makeFormDataRequest(endpoint: endpoint, - version: version, - method: method, - mimeType: mimeType, - filename: filename, - data: data) + public func mediaUpload( + endpoint: Endpoint, + version: Version, + method: String, + mimeType: String, + filename: String, + data: Data + ) async throws -> HTTPURLResponse? { + let request = try makeFormDataRequest( + endpoint: endpoint, + version: version, + method: method, + mimeType: mimeType, + filename: filename, + data: data) let (_, httpResponse) = try await urlSession.data(for: request) return httpResponse as? HTTPURLResponse } - private func makeFormDataRequest(endpoint: Endpoint, - version: Version, - method: String, - mimeType: String, - filename: String, - data: Data) throws -> URLRequest - { + private func makeFormDataRequest( + endpoint: Endpoint, + version: Version, + method: String, + mimeType: String, + filename: String, + data: Data + ) throws -> URLRequest { let url = try makeURL(endpoint: endpoint, forceVersion: version) var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) let boundary = UUID().uuidString - request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + request.setValue( + "multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") let httpBody = NSMutableData() httpBody.append("--\(boundary)\r\n".data(using: .utf8)!) - httpBody.append("Content-Disposition: form-data; name=\"\(filename)\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!) + httpBody.append( + "Content-Disposition: form-data; name=\"\(filename)\"; filename=\"\(filename)\"\r\n".data( + using: .utf8)!) httpBody.append("Content-Type: \(mimeType)\r\n".data(using: .utf8)!) httpBody.append("\r\n".data(using: .utf8)!) httpBody.append(data) @@ -305,7 +331,8 @@ import SwiftUI private func logResponseOnError(httpResponse: URLResponse, data: Data) { if let httpResponse = httpResponse as? HTTPURLResponse, httpResponse.statusCode > 299 { - let error = "HTTP Response error: \(httpResponse.statusCode), response: \(httpResponse), data: \(String(data: data, encoding: .utf8) ?? "")" + let error = + "HTTP Response error: \(httpResponse.statusCode), response: \(httpResponse), data: \(String(data: data, encoding: .utf8) ?? "")" logger.error("\(error)") } } diff --git a/Packages/Network/Sources/Network/DeepLClient.swift b/Packages/Network/Sources/Network/DeepLClient.swift index d3ba47a8..f8e10259 100644 --- a/Packages/Network/Sources/Network/DeepLClient.swift +++ b/Packages/Network/Sources/Network/DeepLClient.swift @@ -49,9 +49,10 @@ public struct DeepLClient: Sendable { let (result, _) = try await URLSession.shared.data(for: request) let response = try decoder.decode(Response.self, from: result) if let translation = response.translations.first { - return .init(content: translation.text.removingPercentEncoding ?? "", - detectedSourceLanguage: translation.detectedSourceLanguage, - provider: "DeepL.com") + return .init( + content: translation.text.removingPercentEncoding ?? "", + detectedSourceLanguage: translation.detectedSourceLanguage, + provider: "DeepL.com") } throw DeepLError.notFound } diff --git a/Packages/Network/Sources/Network/Endpoint/Accounts.swift b/Packages/Network/Sources/Network/Endpoint/Accounts.swift index 536c928a..2870479a 100644 --- a/Packages/Network/Sources/Network/Endpoint/Accounts.swift +++ b/Packages/Network/Sources/Network/Endpoint/Accounts.swift @@ -11,13 +11,14 @@ public enum Accounts: Endpoint { case verifyCredentials case updateCredentialsMedia case updateCredentials(json: UpdateCredentialsData) - case statuses(id: String, - sinceId: String?, - tag: String?, - onlyMedia: Bool, - excludeReplies: Bool, - excludeReblogs: Bool, - pinned: Bool?) + case statuses( + id: String, + sinceId: String?, + tag: String?, + onlyMedia: Bool, + excludeReplies: Bool, + excludeReblogs: Bool, + pinned: Bool?) case relationships(ids: [String]) case follow(id: String, notify: Bool, reblogs: Bool) case unfollow(id: String) @@ -94,7 +95,7 @@ public enum Accounts: Endpoint { switch self { case let .lookup(name): return [ - .init(name: "acct", value: name), + .init(name: "acct", value: name) ] case let .statuses(_, sinceId, tag, onlyMedia, excludeReplies, excludeReblogs, pinned): var params: [URLQueryItem] = [] @@ -198,14 +199,15 @@ public struct UpdateCredentialsData: Encodable, Sendable { public let discoverable: Bool public let fieldsAttributes: [String: FieldData] - public init(displayName: String, - note: String, - source: UpdateCredentialsData.SourceData, - bot: Bool, - locked: Bool, - discoverable: Bool, - fieldsAttributes: [FieldData]) - { + public init( + displayName: String, + note: String, + source: UpdateCredentialsData.SourceData, + bot: Bool, + locked: Bool, + discoverable: Bool, + fieldsAttributes: [FieldData] + ) { self.displayName = displayName self.note = note self.source = source diff --git a/Packages/Network/Sources/Network/Endpoint/Endpoint.swift b/Packages/Network/Sources/Network/Endpoint/Endpoint.swift index f27ddd2a..fac0c4b6 100644 --- a/Packages/Network/Sources/Network/Endpoint/Endpoint.swift +++ b/Packages/Network/Sources/Network/Endpoint/Endpoint.swift @@ -6,8 +6,8 @@ public protocol Endpoint: Sendable { var jsonValue: Encodable? { get } } -public extension Endpoint { - var jsonValue: Encodable? { +extension Endpoint { + public var jsonValue: Encodable? { nil } } diff --git a/Packages/Network/Sources/Network/Endpoint/Lists.swift b/Packages/Network/Sources/Network/Endpoint/Lists.swift index d87e326f..1ec957c4 100644 --- a/Packages/Network/Sources/Network/Endpoint/Lists.swift +++ b/Packages/Network/Sources/Network/Endpoint/Lists.swift @@ -27,10 +27,12 @@ public enum Lists: Endpoint { case .accounts: return [.init(name: "limit", value: String(0))] case let .createList(title, repliesPolicy, exclusive), - let .updateList(_, title, repliesPolicy, exclusive): - return [.init(name: "title", value: title), - .init(name: "replies_policy", value: repliesPolicy.rawValue), - .init(name: "exclusive", value: exclusive ? "true" : "false")] + let .updateList(_, title, repliesPolicy, exclusive): + return [ + .init(name: "title", value: title), + .init(name: "replies_policy", value: repliesPolicy.rawValue), + .init(name: "exclusive", value: exclusive ? "true" : "false"), + ] case let .updateAccounts(_, accounts): var params: [URLQueryItem] = [] for account in accounts { diff --git a/Packages/Network/Sources/Network/Endpoint/Markers.swift b/Packages/Network/Sources/Network/Endpoint/Markers.swift index fb4a6fe8..1acaf2fd 100644 --- a/Packages/Network/Sources/Network/Endpoint/Markers.swift +++ b/Packages/Network/Sources/Network/Endpoint/Markers.swift @@ -12,8 +12,10 @@ public enum Markers: Endpoint { public func queryItems() -> [URLQueryItem]? { switch self { case .markers: - [URLQueryItem(name: "timeline[]", value: "home"), - URLQueryItem(name: "timeline[]", value: "notifications")] + [ + URLQueryItem(name: "timeline[]", value: "home"), + URLQueryItem(name: "timeline[]", value: "notifications"), + ] case let .markNotifications(lastReadId): [URLQueryItem(name: "notifications[last_read_id]", value: lastReadId)] case let .markHome(lastReadId): diff --git a/Packages/Network/Sources/Network/Endpoint/Notifications.swift b/Packages/Network/Sources/Network/Endpoint/Notifications.swift index 3094a8db..82470ff4 100644 --- a/Packages/Network/Sources/Network/Endpoint/Notifications.swift +++ b/Packages/Network/Sources/Network/Endpoint/Notifications.swift @@ -2,10 +2,11 @@ import Foundation import Models public enum Notifications: Endpoint { - case notifications(minId: String?, - maxId: String?, - types: [String]?, - limit: Int) + case notifications( + minId: String?, + maxId: String?, + types: [String]?, + limit: Int) case notificationsForAccount(accountId: String, maxId: String?) case notification(id: String) case policy diff --git a/Packages/Network/Sources/Network/Endpoint/Push.swift b/Packages/Network/Sources/Network/Endpoint/Push.swift index 65692a24..d0c161c3 100644 --- a/Packages/Network/Sources/Network/Endpoint/Push.swift +++ b/Packages/Network/Sources/Network/Endpoint/Push.swift @@ -2,15 +2,16 @@ import Foundation public enum Push: Endpoint { case subscription - case createSub(endpoint: String, - p256dh: Data, - auth: Data, - mentions: Bool, - status: Bool, - reblog: Bool, - follow: Bool, - favorite: Bool, - poll: Bool) + case createSub( + endpoint: String, + p256dh: Data, + auth: Data, + mentions: Bool, + status: Bool, + reblog: Bool, + follow: Bool, + favorite: Bool, + poll: Bool) public func path() -> String { switch self { @@ -24,7 +25,8 @@ public enum Push: Endpoint { case let .createSub(endpoint, p256dh, auth, mentions, status, reblog, follow, favorite, poll): var params: [URLQueryItem] = [] params.append(.init(name: "subscription[endpoint]", value: endpoint)) - params.append(.init(name: "subscription[keys][p256dh]", value: p256dh.base64UrlEncodedString())) + params.append( + .init(name: "subscription[keys][p256dh]", value: p256dh.base64UrlEncodedString())) params.append(.init(name: "subscription[keys][auth]", value: auth.base64UrlEncodedString())) params.append(.init(name: "data[alerts][mention]", value: mentions ? "true" : "false")) params.append(.init(name: "data[alerts][status]", value: status ? "true" : "false")) diff --git a/Packages/Network/Sources/Network/Endpoint/Search.swift b/Packages/Network/Sources/Network/Endpoint/Search.swift index 0bf160bc..5e779c0b 100644 --- a/Packages/Network/Sources/Network/Endpoint/Search.swift +++ b/Packages/Network/Sources/Network/Endpoint/Search.swift @@ -20,7 +20,7 @@ public enum Search: Endpoint { public func queryItems() -> [URLQueryItem]? { switch self { case let .search(query, type, offset, following), - let .accountsSearch(query, type, offset, following): + let .accountsSearch(query, type, offset, following): var params: [URLQueryItem] = [.init(name: "q", value: query)] if let type { params.append(.init(name: "type", value: type.rawValue)) diff --git a/Packages/Network/Sources/Network/Endpoint/ServerFilters.swift b/Packages/Network/Sources/Network/Endpoint/ServerFilters.swift index 519b9e28..6ef90c50 100644 --- a/Packages/Network/Sources/Network/Endpoint/ServerFilters.swift +++ b/Packages/Network/Sources/Network/Endpoint/ServerFilters.swift @@ -29,8 +29,10 @@ public enum ServerFilters: Endpoint { public func queryItems() -> [URLQueryItem]? { switch self { case let .addKeyword(_, keyword, wholeWord): - [.init(name: "keyword", value: keyword), - .init(name: "whole_word", value: wholeWord ? "true" : "false")] + [ + .init(name: "keyword", value: keyword), + .init(name: "whole_word", value: wholeWord ? "true" : "false"), + ] default: nil } @@ -58,11 +60,12 @@ public struct ServerFilterData: Encodable, Sendable { // the expiry of a filter public let expiresIn: String? - public init(title: String, - context: [ServerFilter.Context], - filterAction: ServerFilter.Action, - expiresIn: String?) - { + public init( + title: String, + context: [ServerFilter.Context], + filterAction: ServerFilter.Action, + expiresIn: String? + ) { self.title = title self.context = context self.filterAction = filterAction diff --git a/Packages/Network/Sources/Network/Endpoint/Statuses.swift b/Packages/Network/Sources/Network/Endpoint/Statuses.swift index f651e5c1..d4b77a10 100644 --- a/Packages/Network/Sources/Network/Endpoint/Statuses.swift +++ b/Packages/Network/Sources/Network/Endpoint/Statuses.swift @@ -71,9 +71,11 @@ public enum Statuses: Endpoint { } return nil case let .report(accountId, statusId, comment): - return [.init(name: "account_id", value: accountId), - .init(name: "status_ids[]", value: statusId), - .init(name: "comment", value: comment)] + return [ + .init(name: "account_id", value: accountId), + .init(name: "status_ids[]", value: statusId), + .init(name: "comment", value: comment), + ] default: return nil } @@ -127,15 +129,16 @@ public struct StatusData: Encodable, Sendable { } } - public init(status: String, - visibility: Visibility, - inReplyToId: String? = nil, - spoilerText: String? = nil, - mediaIds: [String]? = nil, - poll: PollData? = nil, - language: String? = nil, - mediaAttributes: [MediaAttribute]? = nil) - { + public init( + status: String, + visibility: Visibility, + inReplyToId: String? = nil, + spoilerText: String? = nil, + mediaIds: [String]? = nil, + poll: PollData? = nil, + language: String? = nil, + mediaAttributes: [MediaAttribute]? = nil + ) { self.status = status self.visibility = visibility self.inReplyToId = inReplyToId diff --git a/Packages/Network/Sources/Network/Endpoint/Timelines.swift b/Packages/Network/Sources/Network/Endpoint/Timelines.swift index c709b6b4..1fabf395 100644 --- a/Packages/Network/Sources/Network/Endpoint/Timelines.swift +++ b/Packages/Network/Sources/Network/Endpoint/Timelines.swift @@ -41,8 +41,9 @@ public enum Timelines: Endpoint { return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId) case let .hashtag(_, additional, maxId, minId): var params = makePaginationParam(sinceId: nil, maxId: maxId, mindId: minId) ?? [] - params.append(contentsOf: (additional ?? []) - .map { URLQueryItem(name: "any[]", value: $0) }) + params.append( + contentsOf: (additional ?? []) + .map { URLQueryItem(name: "any[]", value: $0) }) return params case let .link(url, sinceId, maxId, minId): var params = makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: minId) ?? [] diff --git a/Packages/Network/Sources/Network/InstanceSocialClient.swift b/Packages/Network/Sources/Network/InstanceSocialClient.swift index 821bedeb..228a99de 100644 --- a/Packages/Network/Sources/Network/InstanceSocialClient.swift +++ b/Packages/Network/Sources/Network/InstanceSocialClient.swift @@ -2,8 +2,10 @@ import Foundation import Models public struct InstanceSocialClient: Sendable { - private let authorization = "Bearer 8a4xx3D7Hzu1aFnf18qlkH8oU0oZ5ulabXxoS2FtQtwOy8G0DGQhr5PjTIjBnYAmFrSBuE2CcASjFocxJBonY8XGbLySB7MXd9ssrwlRHUXTQh3Z578lE1OfUtafvhML" - private let listEndpoint = "https://instances.social/api/1.0/instances/list?count=1000&include_closed=false&include_dead=false&min_active_users=500" + private let authorization = + "Bearer 8a4xx3D7Hzu1aFnf18qlkH8oU0oZ5ulabXxoS2FtQtwOy8G0DGQhr5PjTIjBnYAmFrSBuE2CcASjFocxJBonY8XGbLySB7MXd9ssrwlRHUXTQh3Z578lE1OfUtafvhML" + private let listEndpoint = + "https://instances.social/api/1.0/instances/list?count=1000&include_closed=false&include_dead=false&min_active_users=500" private let searchEndpoint = "https://instances.social/api/1.0/instances/search" struct Response: Decodable { @@ -34,8 +36,8 @@ public struct InstanceSocialClient: Sendable { } } -private extension Array where Self.Element == InstanceSocial { - func sorted(by keyword: String) -> Self { +extension Array where Self.Element == InstanceSocial { + fileprivate func sorted(by keyword: String) -> Self { let keyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines) var newArray = self @@ -59,10 +61,11 @@ private extension Array where Self.Element == InstanceSocial { if !keyword.isEmpty { newArray.sort { (lhs: InstanceSocial, rhs: InstanceSocial) in - if - lhs.name.contains(keyword), + if lhs.name.contains(keyword), !rhs.name.contains(keyword) - { return true } + { + return true + } return false } diff --git a/Packages/Network/Sources/Network/OpenAIClient.swift b/Packages/Network/Sources/Network/OpenAIClient.swift index 574a007d..5a73f2a8 100644 --- a/Packages/Network/Sources/Network/OpenAIClient.swift +++ b/Packages/Network/Sources/Network/OpenAIClient.swift @@ -68,18 +68,34 @@ public struct OpenAIClient { var request: OpenAIRequest { switch self { case let .correct(input): - ChatRequest(content: "Fix the spelling and grammar mistakes in the following text: \(input)", temperature: 0.2) + ChatRequest( + content: "Fix the spelling and grammar mistakes in the following text: \(input)", + temperature: 0.2) case let .addTags(input): - ChatRequest(content: "Replace relevant words with camel-cased hashtags in the following text. Don't try to search for context or add hashtags if there is not enough context: \(input)", temperature: 0.1) + ChatRequest( + content: + "Replace relevant words with camel-cased hashtags in the following text. Don't try to search for context or add hashtags if there is not enough context: \(input)", + temperature: 0.1) case let .insertTags(input): - ChatRequest(content: "Return the input with added camel-cased hashtags at the end of the input. Don't try to search for context or add hashtags if there is not enough context: \(input)", temperature: 0.2) + ChatRequest( + content: + "Return the input with added camel-cased hashtags at the end of the input. Don't try to search for context or add hashtags if there is not enough context: \(input)", + temperature: 0.2) case let .shorten(input): ChatRequest(content: "Make a shorter version of this text: \(input)", temperature: 0.5) case let .emphasize(input): ChatRequest(content: "Make this text catchy, more fun: \(input)", temperature: 1) case let .imageDescription(image): - VisionRequest(messages: [.init(content: [.init(type: "text", text: "What’s in this image? Be brief, it's for image alt description on a social network. Don't write in the first person.", imageUrl: nil), - .init(type: "image_url", text: nil, imageUrl: .init(url: image))])]) + VisionRequest(messages: [ + .init(content: [ + .init( + type: "text", + text: + "What’s in this image? Be brief, it's for image alt description on a social network. Don't write in the first person.", + imageUrl: nil), + .init(type: "image_url", text: nil, imageUrl: .init(url: image)), + ]) + ]) } } } diff --git a/Packages/Network/Sources/Network/String.swift b/Packages/Network/Sources/Network/String.swift index 78508528..ce35685a 100644 --- a/Packages/Network/Sources/Network/String.swift +++ b/Packages/Network/Sources/Network/String.swift @@ -1,7 +1,7 @@ import Foundation -public extension String { - func escape() -> String { +extension String { + public func escape() -> String { replacingOccurrences(of: "&", with: "&") .replacingOccurrences(of: "<", with: "<") .replacingOccurrences(of: ">", with: ">") @@ -10,7 +10,7 @@ public extension String { .replacingOccurrences(of: "'", with: "’") } - func URLSafeBase64ToBase64() -> String { + public func URLSafeBase64ToBase64() -> String { var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") let countMod4 = count % 4 diff --git a/Packages/Network/Sources/Network/SubClubClient.swift b/Packages/Network/Sources/Network/SubClubClient.swift index 19e14c68..fc7bc836 100644 --- a/Packages/Network/Sources/Network/SubClubClient.swift +++ b/Packages/Network/Sources/Network/SubClubClient.swift @@ -4,7 +4,7 @@ import Models public struct SubClubClient: Sendable { public enum Endpoint { case user(username: String) - + var path: String { switch self { case .user(let username): @@ -12,13 +12,13 @@ public struct SubClubClient: Sendable { } } } - - public init() { } - + + public init() {} + private var url: String { "https://\(AppInfo.premiumInstance)/" } - + public func getUser(username: String) async -> SubClubUser? { guard let url = URL(string: url.appending(Endpoint.user(username: username).path)) else { return nil diff --git a/Packages/Network/Tests/NetworkTests/NetworkTests.swift b/Packages/Network/Tests/NetworkTests/NetworkTests.swift index eb6dc679..4a018de1 100644 --- a/Packages/Network/Tests/NetworkTests/NetworkTests.swift +++ b/Packages/Network/Tests/NetworkTests/NetworkTests.swift @@ -1,6 +1,7 @@ -@testable import Network import XCTest +@testable import Network + final class NetworkTests: XCTestCase { func testExample() throws { // This is an example of a functional test case. diff --git a/Packages/Notifications/Package.swift b/Packages/Notifications/Package.swift index 2727b932..f709dbd5 100644 --- a/Packages/Notifications/Package.swift +++ b/Packages/Notifications/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "Notifications", targets: ["Notifications"] - ), + ) ], dependencies: [ .package(name: "Network", path: "../Network"), @@ -34,8 +34,8 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] - ), + ) ] ) diff --git a/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift b/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift index 8b1b6320..e725e7b6 100644 --- a/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift +++ b/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift @@ -9,26 +9,29 @@ import Foundation import Models extension [Models.Notification] { - func consolidated(selectedType: Models.Notification.NotificationType?) async -> [ConsolidatedNotification] { + func consolidated(selectedType: Models.Notification.NotificationType?) async + -> [ConsolidatedNotification] + { await withCheckedContinuation { result in DispatchQueue.global().async { let notifications: [ConsolidatedNotification] = Dictionary(grouping: self) { $0.consolidationId(selectedType: selectedType) } - .values - .compactMap { notifications in - guard let notification = notifications.first, - let supportedType = notification.supportedType - else { return nil } + .values + .compactMap { notifications in + guard let notification = notifications.first, + let supportedType = notification.supportedType + else { return nil } - return ConsolidatedNotification(notifications: notifications, - type: supportedType, - createdAt: notification.createdAt, - accounts: notifications.map(\.account), - status: notification.status) - } - .sorted { - $0.createdAt.asDate > $1.createdAt.asDate - } + return ConsolidatedNotification( + notifications: notifications, + type: supportedType, + createdAt: notification.createdAt, + accounts: notifications.map(\.account), + status: notification.status) + } + .sorted { + $0.createdAt.asDate > $1.createdAt.asDate + } result.resume(returning: notifications) } } diff --git a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift index e180fb53..0224c751 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift @@ -23,8 +23,10 @@ struct NotificationRowView: View { .accessibilityHidden(true) } else { makeNotificationIconView(type: notification.type) - .frame(width: AvatarView.FrameConfig.status.width, - height: AvatarView.FrameConfig.status.height) + .frame( + width: AvatarView.FrameConfig.status.width, + height: AvatarView.FrameConfig.status.height + ) .accessibilityHidden(true) } VStack(alignment: .leading, spacing: 0) { @@ -33,7 +35,7 @@ struct NotificationRowView: View { .accessibilityHidden(notification.type == .mention) makeContent(type: notification.type) if notification.type == .follow_request, - followRequests.map(\.id).contains(notification.accounts[0].id) + followRequests.map(\.id).contains(notification.accounts[0].id) { FollowRequestButtons(account: notification.accounts[0]) } @@ -66,7 +68,10 @@ struct NotificationRowView: View { ZStack(alignment: .center) { Circle() .strokeBorder(Color.white, lineWidth: 1) - .background(Circle().foregroundColor(type.tintColor(isPrivate: notification.status?.visibility == .direct))) + .background( + Circle().foregroundColor( + type.tintColor(isPrivate: notification.status?.visibility == .direct)) + ) .frame(width: 24, height: 24) type.icon(isPrivate: notification.status?.visibility == .direct) @@ -96,32 +101,34 @@ struct NotificationRowView: View { } if !reasons.contains(.placeholder) { HStack(spacing: 0) { - EmojiTextApp(.init(stringValue: notification.accounts[0].safeDisplayName), - emojis: notification.accounts[0].emojis, - append: { - (notification.accounts.count > 1 - ? Text("notifications-others-count \(notification.accounts.count - 1)") - .font(.scaledSubheadline) - .fontWeight(.regular) - : Text(" ")) + - Text(type.label(count: notification.accounts.count)) - .font(.scaledSubheadline) - .fontWeight(.regular) + - Text(" ⸱ ") - .font(.scaledFootnote) - .fontWeight(.regular) - .foregroundStyle(.secondary) + - Text(notification.createdAt.relativeFormatted) - .font(.scaledFootnote) - .fontWeight(.regular) - .foregroundStyle(.secondary) - }) - .font(.scaledSubheadline) - .emojiText.size(Font.scaledSubheadlineFont.emojiSize) - .emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset) - .fontWeight(.semibold) - .lineLimit(3) - .fixedSize(horizontal: false, vertical: true) + EmojiTextApp( + .init(stringValue: notification.accounts[0].safeDisplayName), + emojis: notification.accounts[0].emojis, + append: { + (notification.accounts.count > 1 + ? Text("notifications-others-count \(notification.accounts.count - 1)") + .font(.scaledSubheadline) + .fontWeight(.regular) + : Text(" ")) + + Text(type.label(count: notification.accounts.count)) + .font(.scaledSubheadline) + .fontWeight(.regular) + + Text(" ⸱ ") + .font(.scaledFootnote) + .fontWeight(.regular) + .foregroundStyle(.secondary) + + Text(notification.createdAt.relativeFormatted) + .font(.scaledFootnote) + .fontWeight(.regular) + .foregroundStyle(.secondary) + } + ) + .font(.scaledSubheadline) + .emojiText.size(Font.scaledSubheadlineFont.emojiSize) + .emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset) + .fontWeight(.semibold) + .lineLimit(3) + .fixedSize(horizontal: false, vertical: true) if let status = notification.status, notification.type == .mention { Group { Text(" ⸱ ") @@ -156,19 +163,25 @@ struct NotificationRowView: View { if let status = notification.status { HStack { if type == .mention { - StatusRowExternalView(viewModel: .init(status: status, - client: client, - routerPath: routerPath, - showActions: true)) - .environment(\.isMediaCompact, false) + StatusRowExternalView( + viewModel: .init( + status: status, + client: client, + routerPath: routerPath, + showActions: true) + ) + .environment(\.isMediaCompact, false) } else { - StatusRowExternalView(viewModel: .init(status: status, - client: client, - routerPath: routerPath, - showActions: false, - textDisabled: true)) - .lineLimit(4) - .environment(\.isMediaCompact, true) + StatusRowExternalView( + viewModel: .init( + status: status, + client: client, + routerPath: routerPath, + showActions: false, + textDisabled: true) + ) + .lineLimit(4) + .environment(\.isMediaCompact, true) } Spacer() } @@ -180,18 +193,23 @@ struct NotificationRowView: View { .foregroundStyle(.secondary) if type == .follow { - EmojiTextApp(notification.accounts[0].note, - emojis: notification.accounts[0].emojis) - .accessibilityLabel(notification.accounts[0].note.asRawText) - .lineLimit(3) - .font(.scaledCallout) - .emojiText.size(Font.scaledCalloutFont.emojiSize) - .emojiText.baselineOffset(Font.scaledCalloutFont.emojiBaselineOffset) - .foregroundStyle(.secondary) - .environment(\.openURL, OpenURLAction { url in + EmojiTextApp( + notification.accounts[0].note, + emojis: notification.accounts[0].emojis + ) + .accessibilityLabel(notification.accounts[0].note.asRawText) + .lineLimit(3) + .font(.scaledCallout) + .emojiText.size(Font.scaledCalloutFont.emojiSize) + .emojiText.baselineOffset(Font.scaledCalloutFont.emojiBaselineOffset) + .foregroundStyle(.secondary) + .environment( + \.openURL, + OpenURLAction { url in routerPath.handle(url: url) - }) - .accessibilityAddTraits(.isButton) + } + ) + .accessibilityAddTraits(.isButton) } } .contentShape(Rectangle()) diff --git a/Packages/Notifications/Sources/Notifications/NotificationsHeaderFilteredView.swift b/Packages/Notifications/Sources/Notifications/NotificationsHeaderFilteredView.swift index dbb3c0bb..d676c594 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsHeaderFilteredView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsHeaderFilteredView.swift @@ -30,10 +30,12 @@ struct NotificationsHeaderFilteredView: View { routerPath.navigate(to: .notificationsRequests) } .listRowBackground(theme.secondaryBackgroundColor) - .listRowInsets(.init(top: 12, - leading: .layoutPadding, - bottom: 12, - trailing: .layoutPadding)) + .listRowInsets( + .init( + top: 12, + leading: .layoutPadding, + bottom: 12, + trailing: .layoutPadding)) } } } diff --git a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift index 9aed8834..cbb15c28 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift @@ -21,9 +21,10 @@ public struct NotificationsListView: View { let lockedType: Models.Notification.NotificationType? let lockedAccountId: String? - public init(lockedType: Models.Notification.NotificationType? = nil, - lockedAccountId: String? = nil) - { + public init( + lockedType: Models.Notification.NotificationType? = nil, + lockedAccountId: String? = nil + ) { self.lockedType = lockedType self.lockedAccountId = lockedAccountId } @@ -42,7 +43,9 @@ public struct NotificationsListView: View { .listStyle(.plain) .toolbar { ToolbarItem(placement: .principal) { - let title = lockedType?.menuTitle() ?? viewModel.selectedType?.menuTitle() ?? "notifications.navigation-title" + let title = + lockedType?.menuTitle() ?? viewModel.selectedType?.menuTitle() + ?? "notifications.navigation-title" if lockedType == nil { Text(title) .font(.headline) @@ -106,43 +109,43 @@ public struct NotificationsListView: View { .scrollContentBackground(.hidden) .background(theme.primaryBackgroundColor) #endif - .task { - viewModel.client = client - viewModel.currentAccount = account - if let lockedType { - viewModel.isLockedType = true - viewModel.selectedType = lockedType - } else if let lockedAccountId { - viewModel.lockedAccountId = lockedAccountId - } else { - viewModel.loadSelectedType() - } - await viewModel.fetchNotifications(viewModel.selectedType) - await viewModel.fetchPolicy() - } - .refreshable { - SoundEffectManager.shared.playSound(.pull) - HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3)) - await viewModel.fetchNotifications(viewModel.selectedType) - await viewModel.fetchPolicy() - HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7)) - SoundEffectManager.shared.playSound(.refresh) - } - .onChange(of: watcher.latestEvent?.id) { - if let latestEvent = watcher.latestEvent { - viewModel.handleEvent(selectedType: viewModel.selectedType, event: latestEvent) - } - } - .onChange(of: scenePhase) { _, newValue in - switch newValue { - case .active: - Task { - await viewModel.fetchNotifications(viewModel.selectedType) - } - default: - break + .task { + viewModel.client = client + viewModel.currentAccount = account + if let lockedType { + viewModel.isLockedType = true + viewModel.selectedType = lockedType + } else if let lockedAccountId { + viewModel.lockedAccountId = lockedAccountId + } else { + viewModel.loadSelectedType() + } + await viewModel.fetchNotifications(viewModel.selectedType) + await viewModel.fetchPolicy() + } + .refreshable { + SoundEffectManager.shared.playSound(.pull) + HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3)) + await viewModel.fetchNotifications(viewModel.selectedType) + await viewModel.fetchPolicy() + HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7)) + SoundEffectManager.shared.playSound(.refresh) + } + .onChange(of: watcher.latestEvent?.id) { + if let latestEvent = watcher.latestEvent { + viewModel.handleEvent(selectedType: viewModel.selectedType, event: latestEvent) + } + } + .onChange(of: scenePhase) { _, newValue in + switch newValue { + case .active: + Task { + await viewModel.fetchNotifications(viewModel.selectedType) } + default: + break } + } } @ViewBuilder @@ -150,52 +153,71 @@ public struct NotificationsListView: View { switch viewModel.state { case .loading: ForEach(ConsolidatedNotification.placeholders()) { notification in - NotificationRowView(notification: notification, - client: client, - routerPath: routerPath, - followRequests: account.followRequests) - .listRowInsets(.init(top: 12, - leading: .layoutPadding + 4, - bottom: 0, - trailing: .layoutPadding)) + NotificationRowView( + notification: notification, + client: client, + routerPath: routerPath, + followRequests: account.followRequests + ) + .listRowInsets( + .init( + top: 12, + leading: .layoutPadding + 4, + bottom: 0, + trailing: .layoutPadding) + ) #if os(visionOS) - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background)) + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background)) #else - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif - .redacted(reason: .placeholder) - .allowsHitTesting(false) + .redacted(reason: .placeholder) + .allowsHitTesting(false) } case let .display(notifications, nextPageState): if notifications.isEmpty { - PlaceholderView(iconName: "bell.slash", - title: "notifications.empty.title", - message: "notifications.empty.message") + PlaceholderView( + iconName: "bell.slash", + title: "notifications.empty.title", + message: "notifications.empty.message" + ) #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) #endif - .listSectionSeparator(.hidden) + .listSectionSeparator(.hidden) } else { ForEach(notifications) { notification in - NotificationRowView(notification: notification, - client: client, - routerPath: routerPath, - followRequests: account.followRequests) - .listRowInsets(.init(top: 12, - leading: .layoutPadding + 4, - bottom: 6, - trailing: .layoutPadding)) + NotificationRowView( + notification: notification, + client: client, + routerPath: routerPath, + followRequests: account.followRequests + ) + .listRowInsets( + .init( + top: 12, + leading: .layoutPadding + 4, + bottom: 6, + trailing: .layoutPadding) + ) #if os(visionOS) - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(notification.type == .mention && lockedType != .mention ? Material.thick : Material.regular).hoverEffect()) + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle( + notification.type == .mention && lockedType != .mention + ? Material.thick : Material.regular + ).hoverEffect() + ) .listRowHoverEffectDisabled() #else - .listRowBackground(notification.type == .mention && lockedType != .mention ? - theme.secondaryBackgroundColor : theme.primaryBackgroundColor) + .listRowBackground( + notification.type == .mention && lockedType != .mention + ? theme.secondaryBackgroundColor : theme.primaryBackgroundColor) #endif - .id(notification.id) + .id(notification.id) } switch nextPageState { @@ -205,10 +227,13 @@ public struct NotificationsListView: View { NextPageView { try await viewModel.fetchNextPage(viewModel.selectedType) } - .listRowInsets(.init(top: .layoutPadding, - leading: .layoutPadding + 4, - bottom: .layoutPadding, - trailing: .layoutPadding)) + .listRowInsets( + .init( + top: .layoutPadding, + leading: .layoutPadding + 4, + bottom: .layoutPadding, + trailing: .layoutPadding) + ) #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) #endif @@ -216,14 +241,15 @@ public struct NotificationsListView: View { } case .error: - ErrorView(title: "notifications.error.title", - message: "notifications.error.message", - buttonTitle: "action.retry") - { + ErrorView( + title: "notifications.error.title", + message: "notifications.error.message", + buttonTitle: "action.retry" + ) { await viewModel.fetchNotifications(viewModel.selectedType) } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif .listSectionSeparator(.hidden) } diff --git a/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift b/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift index fe0749f0..eba767a0 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift @@ -17,70 +17,95 @@ struct NotificationsPolicyView: View { NavigationStack { Form { Section("notifications.content-filter.title-inline") { - Picker(selection: .init(get: { - policy?.forNotFollowing ?? .drop - }, set: { policy in - self.policy?.forNotFollowing = policy - Task { await updatePolicy() } - })) { + Picker( + selection: .init( + get: { + policy?.forNotFollowing ?? .drop + }, + set: { policy in + self.policy?.forNotFollowing = policy + Task { await updatePolicy() } + }) + ) { pickerMenu } label: { - makePickerLabel(title: "notifications.content-filter.peopleYouDontFollow", - subtitle: "Until you manually approve them") + makePickerLabel( + title: "notifications.content-filter.peopleYouDontFollow", + subtitle: "Until you manually approve them") } - - Picker(selection: .init(get: { - policy?.forNotFollowers ?? .drop - }, set: { policy in - self.policy?.forNotFollowers = policy - Task { await updatePolicy() } - })) { + + Picker( + selection: .init( + get: { + policy?.forNotFollowers ?? .drop + }, + set: { policy in + self.policy?.forNotFollowers = policy + Task { await updatePolicy() } + }) + ) { pickerMenu } label: { - makePickerLabel(title: "notifications.content-filter.peopleNotFollowingYou", - subtitle: "And following you for less than 3 days") + makePickerLabel( + title: "notifications.content-filter.peopleNotFollowingYou", + subtitle: "And following you for less than 3 days") } - - Picker(selection: .init(get: { - policy?.forNewAccounts ?? .drop - }, set: { policy in - self.policy?.forNewAccounts = policy - Task { await updatePolicy() } - })) { + + Picker( + selection: .init( + get: { + policy?.forNewAccounts ?? .drop + }, + set: { policy in + self.policy?.forNewAccounts = policy + Task { await updatePolicy() } + }) + ) { pickerMenu } label: { - makePickerLabel(title: "notifications.content-filter.newAccounts", - subtitle: "Created within the past 30 days") + makePickerLabel( + title: "notifications.content-filter.newAccounts", + subtitle: "Created within the past 30 days") } - - Picker(selection: .init(get: { - policy?.forPrivateMentions ?? .drop - }, set: { policy in - self.policy?.forPrivateMentions = policy - Task { await updatePolicy() } - })) { + + Picker( + selection: .init( + get: { + policy?.forPrivateMentions ?? .drop + }, + set: { policy in + self.policy?.forPrivateMentions = policy + Task { await updatePolicy() } + }) + ) { pickerMenu } label: { - makePickerLabel(title: "notifications.content-filter.privateMentions", - subtitle: "Unless it's in reply to your own mention or if you follow the sender") + makePickerLabel( + title: "notifications.content-filter.privateMentions", + subtitle: "Unless it's in reply to your own mention or if you follow the sender") } - - Picker(selection: .init(get: { - policy?.forLimitedAccounts ?? .drop - }, set: { policy in - self.policy?.forLimitedAccounts = policy - Task { await updatePolicy() } - })) { + + Picker( + selection: .init( + get: { + policy?.forLimitedAccounts ?? .drop + }, + set: { policy in + self.policy?.forLimitedAccounts = policy + Task { await updatePolicy() } + }) + ) { pickerMenu } label: { VStack(alignment: .leading) { - makePickerLabel(title: "Moderated accounts", - subtitle: "Limited by server moderators") + makePickerLabel( + title: "Moderated accounts", + subtitle: "Limited by server moderators") } } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor.opacity(0.3)) + .listRowBackground(theme.primaryBackgroundColor.opacity(0.3)) #endif } .formStyle(.grouped) @@ -97,14 +122,15 @@ struct NotificationsPolicyView: View { .presentationDetents([.height(500)]) .presentationBackground(.thinMaterial) } - + private var pickerMenu: some View { ForEach(NotificationsPolicy.Policy.allCases, id: \.self) { policy in Text(policy.rawValue.capitalized) } } - - private func makePickerLabel(title: LocalizedStringKey, subtitle: LocalizedStringKey) -> some View { + + private func makePickerLabel(title: LocalizedStringKey, subtitle: LocalizedStringKey) -> some View + { VStack(alignment: .leading) { Text(title) .font(.callout) @@ -133,7 +159,8 @@ struct NotificationsPolicyView: View { } do { isUpdating = true - self.policy = try await client.put(endpoint: Notifications.putPolicy(policy: policy), forceVersion: .v2) + self.policy = try await client.put( + endpoint: Notifications.putPolicy(policy: policy), forceVersion: .v2) } catch {} } } diff --git a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift index 481f1098..328fc135 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift @@ -44,7 +44,7 @@ import SwiftUI var selectedType: Models.Notification.NotificationType? { didSet { guard oldValue != selectedType, - client?.id != nil + client?.id != nil else { return } if !isLockedType { @@ -86,20 +86,27 @@ import SwiftUI state = .loading let notifications: [Models.Notification] if let lockedAccountId { - notifications = try await client.get(endpoint: Notifications.notificationsForAccount(accountId: lockedAccountId, - maxId: nil)) + notifications = try await client.get( + endpoint: Notifications.notificationsForAccount( + accountId: lockedAccountId, + maxId: nil)) } else { - notifications = try await client.get(endpoint: Notifications.notifications(minId: nil, - maxId: nil, - types: queryTypes, - limit: Constants.notificationLimit)) + notifications = try await client.get( + endpoint: Notifications.notifications( + minId: nil, + maxId: nil, + types: queryTypes, + limit: Constants.notificationLimit)) } consolidatedNotifications = await notifications.consolidated(selectedType: selectedType) markAsRead() nextPageState = notifications.count < Constants.notificationLimit ? .none : .hasNextPage } else if let firstId = consolidatedNotifications.first?.id { - var newNotifications: [Models.Notification] = await fetchNewPages(minId: firstId, maxPages: 10) - nextPageState = consolidatedNotifications.notificationCount < Constants.notificationLimit ? .none : .hasNextPage + var newNotifications: [Models.Notification] = await fetchNewPages( + minId: firstId, maxPages: 10) + nextPageState = + consolidatedNotifications.notificationCount < Constants.notificationLimit + ? .none : .hasNextPage newNotifications = newNotifications.filter { notification in !consolidatedNotifications.contains(where: { $0.id == notification.id }) } @@ -117,8 +124,9 @@ import SwiftUI markAsRead() withAnimation { - state = .display(notifications: consolidatedNotifications, - nextPageState: consolidatedNotifications.isEmpty ? .none : nextPageState) + state = .display( + notifications: consolidatedNotifications, + nextPageState: consolidatedNotifications.isEmpty ? .none : nextPageState) } } catch { state = .error(error: error) @@ -132,10 +140,12 @@ import SwiftUI var latestMinId = minId do { while let newNotifications: [Models.Notification] = - try await client.get(endpoint: Notifications.notifications(minId: latestMinId, - maxId: nil, - types: queryTypes, - limit: Constants.notificationLimit)), + try await client.get( + endpoint: Notifications.notifications( + minId: latestMinId, + maxId: nil, + types: queryTypes, + limit: Constants.notificationLimit)), !newNotifications.isEmpty, pagesLoaded < maxPages { @@ -156,24 +166,32 @@ 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, - maxId: lastId, - types: queryTypes, - limit: Constants.notificationLimit)) + try await client.get( + endpoint: Notifications.notifications( + minId: nil, + maxId: lastId, + types: queryTypes, + limit: Constants.notificationLimit)) } - await consolidatedNotifications.append(contentsOf: newNotifications.consolidated(selectedType: selectedType)) + await consolidatedNotifications.append( + contentsOf: newNotifications.consolidated(selectedType: selectedType)) if consolidatedNotifications.contains(where: { $0.type == .follow_request }) { await currentAccount?.fetchFollowerRequests() } - state = .display(notifications: consolidatedNotifications, - nextPageState: newNotifications.count < Constants.notificationLimit ? .none : .hasNextPage) + state = .display( + notifications: consolidatedNotifications, + nextPageState: newNotifications.count < Constants.notificationLimit ? .none : .hasNextPage) } func markAsRead() { - guard let client, let id = consolidatedNotifications.first?.notifications.first?.id else { return } + guard let client, let id = consolidatedNotifications.first?.notifications.first?.id else { + return + } Task { do { let _: Marker = try await client.post(endpoint: Markers.markNotifications(lastReadId: id)) @@ -191,14 +209,17 @@ import SwiftUI // if it is not already in the list, // and if it can be shown (no selected type or the same as the received notification type) if lockedAccountId == nil, - let event = event as? StreamEventNotification, - !consolidatedNotifications.flatMap(\.notificationIds).contains(event.notification.id), - selectedType == nil || selectedType?.rawValue == event.notification.type + let event = event as? StreamEventNotification, + !consolidatedNotifications.flatMap(\.notificationIds).contains(event.notification.id), + selectedType == nil || selectedType?.rawValue == event.notification.type { if event.notification.isConsolidable(selectedType: selectedType), - !consolidatedNotifications.isEmpty + !consolidatedNotifications.isEmpty { - if let index = consolidatedNotifications.firstIndex(where: { $0.type == event.notification.supportedType && $0.status?.id == event.notification.status?.id }) { + if let index = consolidatedNotifications.firstIndex(where: { + $0.type == event.notification.supportedType + && $0.status?.id == event.notification.status?.id + }) { let latestConsolidatedNotification = consolidatedNotifications.remove(at: index) await consolidatedNotifications.insert( contentsOf: ([event.notification] + latestConsolidatedNotification.notifications) diff --git a/Packages/StatusKit/Package.swift b/Packages/StatusKit/Package.swift index 3f2575e5..69cf2fba 100644 --- a/Packages/StatusKit/Package.swift +++ b/Packages/StatusKit/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "StatusKit", targets: ["StatusKit"] - ), + ) ], dependencies: [ .package(name: "AppAccount", path: "../AppAccount"), @@ -38,8 +38,8 @@ let package = Package( .product(name: "LRUCache", package: "LRUCache"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] - ), + ) ] ) diff --git a/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailView.swift b/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailView.swift index 648d0bb5..5fdf1657 100644 --- a/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailView.swift @@ -74,30 +74,30 @@ public struct StatusDetailView: View { .scrollContentBackground(.hidden) .background(theme.primaryBackgroundColor) #endif - .onChange(of: viewModel.scrollToId) { _, newValue in - if let newValue { - viewModel.scrollToId = nil - proxy.scrollTo(newValue, anchor: .top) - } + .onChange(of: viewModel.scrollToId) { _, newValue in + if let newValue { + viewModel.scrollToId = nil + proxy.scrollTo(newValue, anchor: .top) } - .onAppear { - guard !isLoaded else { return } - viewModel.client = client - viewModel.routerPath = routerPath - Task { - let result = await viewModel.fetch() - isLoaded = true + } + .onAppear { + guard !isLoaded else { return } + viewModel.client = client + viewModel.routerPath = routerPath + Task { + let result = await viewModel.fetch() + isLoaded = true - if !result { - if let url = viewModel.remoteStatusURL { - await UIApplication.shared.open(url) - } - DispatchQueue.main.async { - _ = routerPath.path.popLast() - } + if !result { + if let url = viewModel.remoteStatusURL { + await UIApplication.shared.open(url) + } + DispatchQueue.main.async { + _ = routerPath.path.popLast() } } } + } } .refreshable { Task { @@ -115,11 +115,13 @@ public struct StatusDetailView: View { private func makeStatusesListView(statuses: [Status]) -> some View { ForEach(statuses) { status in - let (indentationLevel, extraInsets) = viewModel.getIndentationLevel(id: status.id, maxIndent: userPreferences.getRealMaxIndent()) - let viewModel: StatusRowViewModel = .init(status: status, - client: client, - routerPath: routerPath, - scrollToId: $viewModel.scrollToId) + let (indentationLevel, extraInsets) = viewModel.getIndentationLevel( + id: status.id, maxIndent: userPreferences.getRealMaxIndent()) + let viewModel: StatusRowViewModel = .init( + status: status, + client: client, + routerPath: routerPath, + scrollToId: $viewModel.scrollToId) let isFocused = self.viewModel.statusId == status.id StatusRowView(viewModel: viewModel, context: .detail) @@ -141,23 +143,26 @@ public struct StatusDetailView: View { } private var errorView: some View { - ErrorView(title: "status.error.title", - message: "status.error.message", - buttonTitle: "action.retry") - { + ErrorView( + title: "status.error.title", + message: "status.error.message", + buttonTitle: "action.retry" + ) { _ = await viewModel.fetch() } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif .listRowSeparator(.hidden) } private var loadingDetailView: some View { ForEach(Status.placeholders()) { status in - StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath), context: .timeline) - .redacted(reason: .placeholder) - .allowsHitTesting(false) + StatusRowView( + viewModel: .init(status: status, client: client, routerPath: routerPath), context: .timeline + ) + .redacted(reason: .placeholder) + .allowsHitTesting(false) } } @@ -172,14 +177,14 @@ public struct StatusDetailView: View { #if !os(visionOS) .listRowBackground(theme.secondaryBackgroundColor) #endif - .listRowInsets(.init()) + .listRowInsets(.init()) } private var topPaddingView: some View { HStack { EmptyView() } - #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) - #endif + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif .listRowSeparator(.hidden) .listRowInsets(.init()) .frame(height: .layoutPadding) diff --git a/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailViewModel.swift index 9fcbdfe2..6ec8150d 100644 --- a/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailViewModel.swift @@ -13,7 +13,9 @@ import SwiftUI var routerPath: RouterPath? enum State { - case loading, display(statuses: [Status]), error(error: Error) + case loading + case display(statuses: [Status]) + case error(error: Error) } var state: State = .loading @@ -57,11 +59,13 @@ import SwiftUI private func fetchRemoteStatus() async -> Bool { guard let client, let remoteStatusURL else { return false } - let results: SearchResults? = try? await client.get(endpoint: Search.search(query: remoteStatusURL.absoluteString, - type: .statuses, - offset: nil, - following: nil), - forceVersion: .v2) + let results: SearchResults? = try? await client.get( + endpoint: Search.search( + query: remoteStatusURL.absoluteString, + type: .statuses, + offset: nil, + following: nil), + forceVersion: .v2) if let statusId = results?.statuses.first?.id { self.statusId = statusId await fetchStatusDetail(animate: false) @@ -114,7 +118,7 @@ import SwiftUI indentationLevelPreviousCache = [:] for status in statuses { if let inReplyToId = status.inReplyToId, - let prevIndent = indentationLevelPreviousCache[inReplyToId] + let prevIndent = indentationLevelPreviousCache[inReplyToId] { indentationLevelPreviousCache[status.id] = prevIndent + 1 } else { @@ -126,11 +130,11 @@ import SwiftUI func handleEvent(event: any StreamEvent, currentAccount: Account?) { Task { if let event = event as? StreamEventUpdate, - event.status.account.id == currentAccount?.id + event.status.account.id == currentAccount?.id { await fetchStatusDetail(animate: true) } else if let event = event as? StreamEventStatusUpdate, - event.status.account.id == currentAccount?.id + event.status.account.id == currentAccount?.id { await fetchStatusDetail(animate: true) } else if event is StreamEventDelete { @@ -139,7 +143,9 @@ import SwiftUI } } - func getIndentationLevel(id: String, maxIndent: UInt) -> (indentationLevel: UInt, extraInset: Double) { + func getIndentationLevel(id: String, maxIndent: UInt) -> ( + indentationLevel: UInt, extraInset: Double + ) { let level = min(indentationLevelPreviousCache[id] ?? 0, maxIndent) let barSize = Double(level) * 2 diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift index 593d1ff7..30756d2d 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift @@ -97,29 +97,40 @@ extension StatusEditor { Image(systemName: "photo.on.rectangle.angled") } } - .photosPicker(isPresented: $isPhotosPickerPresented, - selection: $viewModel.mediaPickers, - maxSelectionCount: currentInstance.instance?.configuration?.statuses.maxMediaAttachments ?? 4, - matching: .any(of: [.images, .videos]), - photoLibrary: .shared()) - .fileImporter(isPresented: $isFileImporterPresented, - allowedContentTypes: [.image, .video, .movie], - allowsMultipleSelection: true) - { result in + .photosPicker( + isPresented: $isPhotosPickerPresented, + selection: $viewModel.mediaPickers, + maxSelectionCount: currentInstance.instance?.configuration?.statuses.maxMediaAttachments + ?? 4, + matching: .any(of: [.images, .videos]), + photoLibrary: .shared() + ) + .fileImporter( + isPresented: $isFileImporterPresented, + allowedContentTypes: [.image, .video, .movie], + allowsMultipleSelection: true + ) { result in if let urls = try? result.get() { viewModel.processURLs(urls: urls) } } - .fullScreenCover(isPresented: $isCameraPickerPresented, content: { - CameraPickerView(selectedImage: .init(get: { - nil - }, set: { image in - if let image { - viewModel.processCameraPhoto(image: image) - } - })) - .background(.black) - }) + .fullScreenCover( + isPresented: $isCameraPickerPresented, + content: { + CameraPickerView( + selectedImage: .init( + get: { + nil + }, + set: { image in + if let image { + viewModel.processCameraPhoto(image: image) + } + }) + ) + .background(.black) + } + ) .accessibilityLabel("accessibility.editor.button.attach-photo") .disabled(viewModel.showPoll) @@ -137,7 +148,8 @@ extension StatusEditor { } label: { // This is a workaround for an apparent bug in the `face.smiling` SF Symbol. // See https://github.com/Dimillian/IceCubesApp/issues/1193 - let customEmojiSheetIconName = colorScheme == .light ? "face.smiling" : "face.smiling.inverse" + let customEmojiSheetIconName = + colorScheme == .light ? "face.smiling" : "face.smiling.inverse" Image(systemName: customEmojiSheetIconName) } .accessibilityLabel("accessibility.editor.button.custom-emojis") @@ -169,13 +181,17 @@ extension StatusEditor { private var canAddNewSEVM: Bool { guard followUpSEVMs.count < 5 else { return false } - if followUpSEVMs.isEmpty, // there is only mainSEVM on the editor - !focusedSEVM.statusText.string.isEmpty // focusedSEVM is also mainSEVM - { return true } + if followUpSEVMs.isEmpty, // there is only mainSEVM on the editor + !focusedSEVM.statusText.string.isEmpty // focusedSEVM is also mainSEVM + { + return true + } if let lastSEVMs = followUpSEVMs.last, - !lastSEVMs.statusText.string.isEmpty - { return true } + !lastSEVMs.statusText.string.isEmpty + { + return true + } return false } @@ -186,7 +202,8 @@ extension StatusEditor { Button { Task { isLoadingAIRequest = true - await focusedSEVM.runOpenAI(prompt: prompt.toRequestPrompt(text: focusedSEVM.statusText.string)) + await focusedSEVM.runOpenAI( + prompt: prompt.toRequestPrompt(text: focusedSEVM.statusText.string)) isLoadingAIRequest = false } } label: { diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/AutoCompleteView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/AutoCompleteView.swift index 62dcd02c..a9a81926 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/AutoCompleteView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/AutoCompleteView.swift @@ -19,9 +19,8 @@ extension StatusEditor { @Query(sort: \RecentTag.lastUse, order: .reverse) var recentTags: [RecentTag] var body: some View { - if !viewModel.mentionsSuggestions.isEmpty || - !viewModel.tagsSuggestions.isEmpty || - (viewModel.showRecentsTagsInline && !recentTags.isEmpty) + if !viewModel.mentionsSuggestions.isEmpty || !viewModel.tagsSuggestions.isEmpty + || (viewModel.showRecentsTagsInline && !recentTags.isEmpty) { VStack { HStack { @@ -31,9 +30,11 @@ extension StatusEditor { Self.MentionsView(viewModel: viewModel) } else { if viewModel.showRecentsTagsInline { - Self.RecentTagsView(viewModel: viewModel, isTagSuggestionExpanded: $isTagSuggestionExpanded) + Self.RecentTagsView( + viewModel: viewModel, isTagSuggestionExpanded: $isTagSuggestionExpanded) } else { - Self.RemoteTagsView(viewModel: viewModel, isTagSuggestionExpanded: $isTagSuggestionExpanded) + Self.RemoteTagsView( + viewModel: viewModel, isTagSuggestionExpanded: $isTagSuggestionExpanded) } } } @@ -47,14 +48,17 @@ extension StatusEditor { isTagSuggestionExpanded.toggle() } } label: { - Image(systemName: isTagSuggestionExpanded ? "chevron.down.circle" : "chevron.up.circle") - .padding(.trailing, 8) + Image( + systemName: isTagSuggestionExpanded ? "chevron.down.circle" : "chevron.up.circle" + ) + .padding(.trailing, 8) } } } .frame(height: 40) if isTagSuggestionExpanded { - Self.ExpandedView(viewModel: viewModel, isTagSuggestionExpanded: $isTagSuggestionExpanded) + Self.ExpandedView( + viewModel: viewModel, isTagSuggestionExpanded: $isTagSuggestionExpanded) } } .background(.thinMaterial) diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/ExpandedView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/ExpandedView.swift index 20faeea1..bed7294d 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/ExpandedView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/ExpandedView.swift @@ -71,7 +71,9 @@ extension StatusEditor.AutoCompleteView { ForEach(currentAccount.tags) { tag in HStack { Button { - if let index = recentTags.firstIndex(where: { $0.title.lowercased() == tag.name.lowercased() }) { + if let index = recentTags.firstIndex(where: { + $0.title.lowercased() == tag.name.lowercased() + }) { recentTags[index].lastUse = Date() } else { context.insert(RecentTag(title: tag.name)) diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/MentionsView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/MentionsView.swift index a3ee1dd7..fb79d9f7 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/MentionsView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/MentionsView.swift @@ -19,13 +19,15 @@ extension StatusEditor.AutoCompleteView { HStack { AvatarView(account.avatar, config: AvatarView.FrameConfig.badge) VStack(alignment: .leading) { - EmojiTextApp(.init(stringValue: account.safeDisplayName), - emojis: account.emojis) - .emojiText.size(Font.scaledFootnoteFont.emojiSize) - .emojiText.baselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset) - .font(.scaledFootnote) - .fontWeight(.bold) - .foregroundColor(theme.labelColor) + EmojiTextApp( + .init(stringValue: account.safeDisplayName), + emojis: account.emojis + ) + .emojiText.size(Font.scaledFootnoteFont.emojiSize) + .emojiText.baselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset) + .font(.scaledFootnote) + .fontWeight(.bold) + .foregroundColor(theme.labelColor) Text("@\(account.acct)") .font(.scaledFootnote) .foregroundStyle(theme.tintColor) diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/RemoteTagsView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/RemoteTagsView.swift index bf58412f..7b0001d3 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/RemoteTagsView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AutoComplete/RemoteTagsView.swift @@ -22,7 +22,9 @@ extension StatusEditor.AutoCompleteView { isTagSuggestionExpanded = false viewModel.selectHashtagSuggestion(tag: tag.name) } - if let index = recentTags.firstIndex(where: { $0.title.lowercased() == tag.name.lowercased() }) { + if let index = recentTags.firstIndex(where: { + $0.title.lowercased() == tag.name.lowercased() + }) { recentTags[index].lastUse = Date() } else { context.insert(RecentTag(title: tag.name)) diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/CameraPickerView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/CameraPickerView.swift index 85800b4a..cfd1520a 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/CameraPickerView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/CameraPickerView.swift @@ -13,7 +13,10 @@ extension StatusEditor { self.picker = picker } - func imagePickerController(_: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + func imagePickerController( + _: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] + ) { guard let selectedImage = info[.originalImage] as? UIImage else { return } picker.selectedImage = selectedImage picker.dismiss() diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/Compressor.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/Compressor.swift index 4d8fe9c0..1c5a8285 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/Compressor.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/Compressor.swift @@ -2,8 +2,8 @@ import AVFoundation import Foundation import UIKit -public extension StatusEditor { - actor Compressor { +extension StatusEditor { + public actor Compressor { public init() {} enum CompressorError: Error { @@ -18,17 +18,19 @@ public extension StatusEditor { return } - let maxPixelSize: Int = if Bundle.main.bundlePath.hasSuffix(".appex") { - 1536 - } else { - 4096 - } + let maxPixelSize: Int = + if Bundle.main.bundlePath.hasSuffix(".appex") { + 1536 + } else { + 4096 + } - let downsampleOptions = [ - kCGImageSourceCreateThumbnailFromImageAlways: true, - kCGImageSourceCreateThumbnailWithTransform: true, - kCGImageSourceThumbnailMaxPixelSize: maxPixelSize, - ] as [CFString: Any] as CFDictionary + let downsampleOptions = + [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: maxPixelSize, + ] as [CFString: Any] as CFDictionary guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { continuation.resume(returning: nil) @@ -36,7 +38,10 @@ public extension StatusEditor { } let data = NSMutableData() - guard let imageDestination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else { + guard + let imageDestination = CGImageDestinationCreateWithData( + data, UTType.jpeg.identifier as CFString, 1, nil) + else { continuation.resume(returning: nil) return } @@ -46,9 +51,10 @@ public extension StatusEditor { return (utType as String) == UTType.png.identifier }() - let destinationProperties = [ - kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75, - ] as CFDictionary + let destinationProperties = + [ + kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75 + ] as CFDictionary CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties) CGImageDestinationFinalize(imageDestination) @@ -70,8 +76,10 @@ public extension StatusEditor { let widthFactor = image.size.width / maxWidth let maxFactor = max(heightFactor, widthFactor) - image = image.resized(to: .init(width: image.size.width / maxFactor, - height: image.size.height / maxFactor)) + image = image.resized( + to: .init( + width: image.size.width / maxFactor, + height: image.size.height / maxFactor)) } guard var imageData = image.jpegData(compressionQuality: 0.8) else { @@ -82,7 +90,8 @@ public extension StatusEditor { if imageData.count > maxSize { while imageData.count > maxSize && compressionQualityFactor >= 0 { guard let compressedImage = UIImage(data: imageData), - let compressedData = compressedImage.jpegData(compressionQuality: compressionQualityFactor) + let compressedData = compressedImage.jpegData( + compressionQuality: compressionQualityFactor) else { throw CompressorError.noData } @@ -102,16 +111,19 @@ public extension StatusEditor { func compressVideo(_ url: URL) async -> URL? { await withCheckedContinuation { continuation in let urlAsset = AVURLAsset(url: url, options: nil) - let presetName: String = if Bundle.main.bundlePath.hasSuffix(".appex") { - AVAssetExportPreset1280x720 - } else { - AVAssetExportPreset1920x1080 - } - guard let exportSession = AVAssetExportSession(asset: urlAsset, presetName: presetName) else { + let presetName: String = + if Bundle.main.bundlePath.hasSuffix(".appex") { + AVAssetExportPreset1280x720 + } else { + AVAssetExportPreset1920x1080 + } + guard let exportSession = AVAssetExportSession(asset: urlAsset, presetName: presetName) + else { continuation.resume(returning: nil) return } - let outputURL = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(url.pathExtension)") + let outputURL = URL.temporaryDirectory.appending( + path: "\(UUID().uuidString).\(url.pathExtension)") exportSession.outputURL = outputURL exportSession.outputFileType = .mp4 exportSession.shouldOptimizeForNetworkUse = true diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/CustomEmojisView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/CustomEmojisView.swift index 3a7e90ba..835d3093 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/CustomEmojisView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/CustomEmojisView.swift @@ -26,7 +26,9 @@ extension StatusEditor { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) - .accessibilityLabel(emoji.shortcode.replacingOccurrences(of: "_", with: " ")) + .accessibilityLabel( + emoji.shortcode.replacingOccurrences(of: "_", with: " ") + ) .accessibilityAddTraits(.isButton) } else if state.isLoading { Rectangle() diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/LangButton.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/LangButton.swift index 23147e47..bd100280 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/LangButton.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/LangButton.swift @@ -31,7 +31,9 @@ extension StatusEditor { } .buttonStyle(.bordered) .onAppear { - viewModel.setInitialLanguageSelection(preference: preferences.recentlyUsedLanguages.first ?? preferences.serverPreferences?.postLanguage) + viewModel.setInitialLanguageSelection( + preference: preferences.recentlyUsedLanguages.first + ?? preferences.serverPreferences?.postLanguage) } .accessibilityLabel("accessibility.editor.button.language") .sheet(isPresented: $isLanguageSheetDisplayed) { @@ -69,7 +71,8 @@ extension StatusEditor { } @ViewBuilder - private func languageTextView(isoCode: String, nativeName: String?, name: String?) -> some View { + private func languageTextView(isoCode: String, nativeName: String?, name: String?) -> some View + { if let nativeName, let name { Text("\(nativeName) (\(name))") } else { @@ -107,7 +110,9 @@ extension StatusEditor { } private var otherLanguages: [Language] { - Language.allAvailableLanguages.filter { !preferences.recentlyUsedLanguages.contains($0.isoCode) } + Language.allAvailableLanguages.filter { + !preferences.recentlyUsedLanguages.contains($0.isoCode) + } } private func languageSearchResult(query: String) -> [Language] { diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaEditView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaEditView.swift index 529b0c2f..ab641477 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaEditView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaEditView.swift @@ -30,10 +30,12 @@ extension StatusEditor { NavigationStack { Form { Section { - TextField("status.editor.media.image-description", - text: $imageDescription, - axis: .vertical) - .focused($isFieldFocused) + TextField( + "status.editor.media.image-description", + text: $imageDescription, + axis: .vertical + ) + .focused($isFieldFocused) if imageDescription.isEmpty { generateButton } @@ -83,13 +85,15 @@ extension StatusEditor { isUpdating = true if currentInstance.isEditAltTextSupported, viewModel.mode.isEditing { Task { - await viewModel.editDescription(container: container, description: imageDescription) + await viewModel.editDescription( + container: container, description: imageDescription) dismiss() isUpdating = false } } else { Task { - await viewModel.addDescription(container: container, description: imageDescription) + await viewModel.addDescription( + container: container, description: imageDescription) dismiss() isUpdating = false } @@ -141,7 +145,7 @@ extension StatusEditor { } } #if canImport(_Translation_SwiftUI) - .addTranslateView(isPresented: $showTranslateView, text: imageDescription) + .addTranslateView(isPresented: $showTranslateView, text: imageDescription) #endif } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaView.swift index 880f10ca..f41de3d4 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaView.swift @@ -78,7 +78,9 @@ extension StatusEditor { } else { makeMediaItem(at: 0) } - } else { pixel(at: 0) } + } else { + pixel(at: 0) + } if count > 1 { makeMediaItem(at: 1) } else { pixel(at: 1) } if count > 2 { makeMediaItem(at: 2) } else { pixel(at: 2) } if count > 3 { makeMediaItem(at: 3) } else { pixel(at: 3) } @@ -174,8 +176,9 @@ extension StatusEditor { Button { editingMediaContainer = container } label: { - Label(container.mediaAttachment?.description?.isEmpty == false ? - "status.editor.description.edit" : "status.editor.description.add", + Label( + container.mediaAttachment?.description?.isEmpty == false + ? "status.editor.description.edit" : "status.editor.description.add", systemImage: "pencil.line") } } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/PollView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/PollView.swift index 846e6264..e3cf62d0 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/PollView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/PollView.swift @@ -24,7 +24,7 @@ extension StatusEditor { @Bindable var viewModel = viewModel let count = viewModel.pollOptions.count VStack { - ForEach(0 ..< count, id: \.self) { index in + ForEach(0.. String { +extension URL { + public func mimeType() -> String { if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType { mimeType } else { diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Drafts/DraftsListView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Drafts/DraftsListView.swift index d8fb1e45..ba1c07b0 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Drafts/DraftsListView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Drafts/DraftsListView.swift @@ -33,9 +33,9 @@ extension StatusEditor { } } #if os(visionOS) - .foregroundStyle(theme.labelColor) + .foregroundStyle(theme.labelColor) #else - .listRowBackground(theme.primaryBackgroundColor.opacity(0.4)) + .listRowBackground(theme.primaryBackgroundColor.opacity(0.4)) #endif } .onDelete { indexes in diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/EditorFocusState.swift b/Packages/StatusKit/Sources/StatusKit/Editor/EditorFocusState.swift index 0b644c30..159544e5 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/EditorFocusState.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/EditorFocusState.swift @@ -2,6 +2,7 @@ import SwiftUI extension StatusEditor { enum EditorFocusState: Hashable { - case main, followUp(index: UUID) + case main + case followUp(index: UUID) } } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/EditorView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/EditorView.swift index 70b56db1..f373108e 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/EditorView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/EditorView.swift @@ -55,7 +55,7 @@ extension StatusEditor { } } #if !os(visionOS) - .background(theme.primaryBackgroundColor) + .background(theme.primaryBackgroundColor) #endif .focused($editorFocusState, equals: assignedFocusState) .onAppear { setupViewModel() } @@ -77,9 +77,10 @@ extension StatusEditor { if let account = currentAccount.account, !viewModel.mode.isEditing { HStack { if viewModel.mode.isInShareExtension { - AppAccountsSelectorView(routerPath: RouterPath(), - accountCreationEnabled: false, - avatarConfig: .status) + AppAccountsSelectorView( + routerPath: RouterPath(), + accountCreationEnabled: false, + avatarConfig: .status) } else { AvatarView(account.avatar, config: AvatarView.FrameConfig.status) .environment(theme) @@ -87,8 +88,10 @@ extension StatusEditor { } VStack(alignment: .leading, spacing: 4) { - PrivacyMenu(visibility: $viewModel.visibility, tint: isMain ? theme.tintColor : .secondary) - .disabled(!isMain) + PrivacyMenu( + visibility: $viewModel.visibility, tint: isMain ? theme.tintColor : .secondary + ) + .disabled(!isMain) Text("@\(account.acct)@\(appAccounts.currentClient.server)") .font(.scaledFootnote) @@ -116,7 +119,11 @@ extension StatusEditor { $viewModel.statusText, getTextView: { textView in viewModel.textView = textView } ) - .placeholder(String(localized: isMain ? "status.editor.text.placeholder" : "status.editor.follow-up.text.placeholder")) + .placeholder( + String( + localized: isMain + ? "status.editor.text.placeholder" : "status.editor.follow-up.text.placeholder") + ) .setKeyboardType(preferences.isSocialKeyboardEnabled ? .twitter : .default) .padding(.horizontal, .layoutPadding) .padding(.vertical) @@ -126,21 +133,26 @@ extension StatusEditor { private var embeddedStatus: some View { if let status = viewModel.replyToStatus { Divider().padding(.vertical, .statusComponentSpacing) - StatusRowView(viewModel: .init(status: status, - client: client, - routerPath: RouterPath(), - showActions: false), - context: .timeline) - .accessibilityLabel(status.content.asRawText) - .environment(RouterPath()) - .allowsHitTesting(false) - .environment(\.isStatusFocused, false) - .environment(\.isModal, true) - .padding(.horizontal, .layoutPadding) - .padding(.vertical, .statusComponentSpacing) + StatusRowView( + viewModel: .init( + status: status, + client: client, + routerPath: RouterPath(), + showActions: false), + context: .timeline + ) + .accessibilityLabel(status.content.asRawText) + .environment(RouterPath()) + .allowsHitTesting(false) + .environment(\.isStatusFocused, false) + .environment(\.isModal, true) + .padding(.horizontal, .layoutPadding) + .padding(.vertical, .statusComponentSpacing) #if os(visionOS) - .background(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background)) + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background) + ) .buttonStyle(.plain) .padding(.layoutPadding) #endif @@ -162,7 +174,9 @@ extension StatusEditor { @ViewBuilder private var characterCountAndLangView: some View { - let value = (currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) + viewModel.statusTextCharacterLength + let value = + (currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) + + viewModel.statusTextCharacterLength HStack(alignment: .center) { LangButton(viewModel: viewModel) .padding(.leading, .layoutPadding) @@ -185,7 +199,9 @@ extension StatusEditor { } isSpoilerTextFocused = viewModel.id } label: { - Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill" : "exclamationmark.triangle") + Image( + systemName: viewModel.spoilerOn + ? "exclamationmark.triangle.fill" : "exclamationmark.triangle") } .buttonStyle(.bordered) .accessibilityLabel("accessibility.editor.button.spoiler") diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/MainView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/MainView.swift index 3fdb15ca..d4cc7b70 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/MainView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/MainView.swift @@ -10,9 +10,9 @@ import StoreKit import SwiftUI import UIKit -public extension StatusEditor { +extension StatusEditor { @MainActor - struct MainView: View { + public struct MainView: View { @Environment(AppAccountsManager.self) private var appAccounts @Environment(CurrentAccount.self) private var currentAccount @Environment(Theme.self) private var theme @@ -27,8 +27,10 @@ public extension StatusEditor { private var focusedSEVM: ViewModel { if case let .followUp(id) = editorFocusState, - let sevm = followUpSEVMs.first(where: { $0.id == id }) - { return sevm } + let sevm = followUpSEVMs.first(where: { $0.id == id }) + { + return sevm + } return mainSEVM } @@ -76,71 +78,79 @@ public extension StatusEditor { #if !os(visionOS) .background(theme.primaryBackgroundColor) #endif - .safeAreaInset(edge: .bottom) { - AutoCompleteView(viewModel: focusedSEVM) - } + .safeAreaInset(edge: .bottom) { + AutoCompleteView(viewModel: focusedSEVM) + } #if os(visionOS) .ornament(attachmentAnchor: .scene(.leading)) { - AccessoryView(focusedSEVM: focusedSEVM, - followUpSEVMs: $followUpSEVMs) + AccessoryView( + focusedSEVM: focusedSEVM, + followUpSEVMs: $followUpSEVMs) } #else .safeAreaInset(edge: .bottom) { - if presentationDetent == .large || presentationDetent == .medium { - AccessoryView(focusedSEVM: focusedSEVM, - followUpSEVMs: $followUpSEVMs) - } + 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 - } - } + .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/PrivacyMenu.swift b/Packages/StatusKit/Sources/StatusKit/Editor/PrivacyMenu.swift index e431393b..ff9d8ca1 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/PrivacyMenu.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/PrivacyMenu.swift @@ -9,7 +9,9 @@ extension StatusEditor { var body: some View { Menu { ForEach(Models.Visibility.allCases, id: \.self) { vis in - Button { visibility = vis } label: { + Button { + visibility = vis + } label: { Label(vis.title, systemImage: vis.iconName) } } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ToolbarItems.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ToolbarItems.swift index 008e7314..8be2e9ba 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ToolbarItems.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ToolbarItems.swift @@ -48,7 +48,9 @@ extension StatusEditor { Button { Task { mainSEVM.evaluateLanguages() - if preferences.autoDetectPostLanguage, let _ = mainSEVM.languageConfirmationDialogLanguages { + if preferences.autoDetectPostLanguage, + mainSEVM.languageConfirmationDialogLanguages != nil + { isLanguageConfirmPresented = true } else { await postAllStatus() @@ -61,9 +63,11 @@ extension StatusEditor { .buttonStyle(.borderedProminent) .disabled(!mainSEVM.canPost || mainSEVM.isPosting) .keyboardShortcut(.return, modifiers: .command) - .confirmationDialog("", isPresented: $isLanguageConfirmPresented, actions: { - languageConfirmationDialog - }) + .confirmationDialog( + "", isPresented: $isLanguageConfirmPresented, + actions: { + languageConfirmationDialog + }) } ToolbarItem(placement: .navigationBarLeading) { @@ -72,8 +76,9 @@ extension StatusEditor { isDismissAlertPresented = true } else { close() - NotificationCenter.default.post(name: .shareSheetClose, - object: nil) + NotificationCenter.default.post( + name: .shareSheetClose, + object: nil) } } label: { Image(systemName: "xmark") @@ -85,14 +90,16 @@ extension StatusEditor { actions: { Button("status.draft.delete", role: .destructive) { close() - NotificationCenter.default.post(name: .shareSheetClose, - object: nil) + NotificationCenter.default.post( + name: .shareSheetClose, + object: nil) } Button("status.draft.save") { context.insert(Draft(content: mainSEVM.statusText.string)) close() - NotificationCenter.default.post(name: .shareSheetClose, - object: nil) + NotificationCenter.default.post( + name: .shareSheetClose, + object: nil) } Button("action.cancel", role: .cancel) {} } @@ -109,7 +116,9 @@ extension StatusEditor { NotificationCenter.default.post(name: .shareSheetClose, object: nil) #if !targetEnvironment(macCatalyst) if !mainSEVM.mode.isInShareExtension, !preferences.requestedReview { - if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { + if let scene = UIApplication.shared.connectedScenes.first(where: { + $0.activationState == .foregroundActive + }) as? UIWindowScene { SKStoreReviewController.requestReview(in: scene) } preferences.requestedReview = true @@ -139,9 +148,10 @@ extension StatusEditor { @ViewBuilder private var languageConfirmationDialog: some View { - if let (detected: detected, selected: selected) = mainSEVM.languageConfirmationDialogLanguages, - let detectedLong = Locale.current.localizedString(forLanguageCode: detected), - let selectedLong = Locale.current.localizedString(forLanguageCode: selected) + if let (detected: detected, selected: selected) = mainSEVM + .languageConfirmationDialogLanguages, + let detectedLong = Locale.current.localizedString(forLanguageCode: detected), + let selectedLong = Locale.current.localizedString(forLanguageCode: selected) { Button("status.editor.language-select.confirmation.detected-\(detectedLong)") { mainSEVM.selectedLanguage = detected @@ -160,13 +170,16 @@ extension StatusEditor { } private var draftsListView: some View { - DraftsListView(selectedDraft: .init(get: { - nil - }, set: { draft in - if let draft { - focusedSEVM.insertStatusText(text: draft.content) - } - })) + DraftsListView( + selectedDraft: .init( + get: { + nil + }, + set: { draft in + if let draft { + focusedSEVM.insertStatusText(text: draft.content) + } + })) } } } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/Coordinator.swift b/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/Coordinator.swift index 46a0bcff..0684982c 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/Coordinator.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/Coordinator.swift @@ -14,11 +14,12 @@ extension TextView.Representable { var getTextView: ((UITextView) -> Void)? - init(text: Binding, - calculatedHeight: Binding, - sizeCategory: ContentSizeCategory, - getTextView: ((UITextView) -> Void)?) - { + init( + text: Binding, + calculatedHeight: Binding, + sizeCategory: ContentSizeCategory, + getTextView: ((UITextView) -> Void)? + ) { textView = UIKitTextView() textView.backgroundColor = .clear textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) @@ -59,7 +60,8 @@ extension TextView.Representable { func textViewDidChange(_ textView: UITextView) { DispatchQueue.main.async { - self.text.wrappedValue = NSMutableAttributedString(attributedString: textView.attributedText) + self.text.wrappedValue = NSMutableAttributedString( + attributedString: textView.attributedText) self.recalculateHeight() } } @@ -78,10 +80,11 @@ extension TextView.Representable.Coordinator { } private func recalculateHeight() { - let newSize = textView.sizeThatFits(CGSize(width: textView.frame.width, height: .greatestFiniteMagnitude)) + let newSize = textView.sizeThatFits( + CGSize(width: textView.frame.width, height: .greatestFiniteMagnitude)) guard calculatedHeight.wrappedValue != newSize.height else { return } - DispatchQueue.main.async { // call in next render cycle. + DispatchQueue.main.async { // call in next render cycle. self.calculatedHeight.wrappedValue = newSize.height } } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/Modifiers.swift b/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/Modifiers.swift index 65ac313e..88f1e970 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/Modifiers.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/Modifiers.swift @@ -1,9 +1,9 @@ import SwiftUI -public extension TextView { +extension TextView { /// Specify a placeholder text /// - Parameter placeholder: The placeholder text - func placeholder(_ placeholder: String) -> TextView { + public func placeholder(_ placeholder: String) -> TextView { self.placeholder(placeholder) { $0 } } @@ -15,7 +15,7 @@ public extension TextView { /// .placeholder("placeholder") { view in /// view.foregroundColor(.red) /// } - func placeholder(_ placeholder: String, _ configure: (Text) -> some View) -> TextView { + public func placeholder(_ placeholder: String, _ configure: (Text) -> some View) -> TextView { var view = self let text = Text(placeholder) view.placeholderView = AnyView(configure(text)) @@ -24,13 +24,13 @@ public extension TextView { } /// Specify a custom placeholder view - func placeholder(_ placeholder: some View) -> TextView { + public func placeholder(_ placeholder: some View) -> TextView { var view = self view.placeholderView = AnyView(placeholder) return view } - func setKeyboardType(_ keyboardType: UIKeyboardType) -> TextView { + public func setKeyboardType(_ keyboardType: UIKeyboardType) -> TextView { var view = self view.keyboard = keyboardType return view diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/TextView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/TextView.swift index ef76e7f1..72d0065a 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/TextView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/UITextView/TextView.swift @@ -19,9 +19,10 @@ public struct TextView: View { /// Makes a new TextView that supports `NSAttributedString` /// - Parameters: /// - text: A binding to the attributed text - public init(_ text: Binding, - getTextView: ((UITextView) -> Void)? = nil) - { + public init( + _ text: Binding, + getTextView: ((UITextView) -> Void)? = nil + ) { _text = text _isEmpty = Binding( get: { text.wrappedValue.string.isEmpty }, @@ -42,7 +43,9 @@ public struct TextView: View { minHeight: calculatedHeight, maxHeight: calculatedHeight ) - .accessibilityValue($text.wrappedValue.string.isEmpty ? (placeholderText ?? "") : $text.wrappedValue.string) + .accessibilityValue( + $text.wrappedValue.string.isEmpty ? (placeholderText ?? "") : $text.wrappedValue.string + ) .background( placeholderView? .foregroundColor(Color(.placeholderText)) @@ -60,7 +63,8 @@ public struct TextView: View { final class UIKitTextView: UITextView { override var keyCommands: [UIKeyCommand]? { (super.keyCommands ?? []) + [ - UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(escape(_:))), + UIKeyCommand( + input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(escape(_:))) ] } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift index e8d1f5d1..5306f3be 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift @@ -2,8 +2,8 @@ import Models import SwiftUI import UIKit -public extension StatusEditor.ViewModel { - enum Mode { +extension StatusEditor.ViewModel { + public enum Mode { case replyTo(status: Status) case new(text: String?, visibility: Models.Visibility) case edit(status: Status) diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift index 39c8f26a..c3d00d97 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift @@ -7,9 +7,9 @@ import Network import PhotosUI import SwiftUI -public extension StatusEditor { +extension StatusEditor { @MainActor - @Observable class ViewModel: NSObject, Identifiable { + @Observable public class ViewModel: NSObject, Identifiable { public let id = UUID() var mode: Mode @@ -95,7 +95,8 @@ public extension StatusEditor { mediaPickers = mediaPickers.prefix(4).map { $0 } } - let removedIDs = oldValue + let removedIDs = + oldValue .filter { !mediaPickers.contains($0) } .compactMap(\.itemIdentifier) mediaContainers.removeAll { removedIDs.contains($0.id) } @@ -132,8 +133,8 @@ public extension StatusEditor { var allMediaHasDescription: Bool { var everyMediaHasAltText = true for mediaContainer in mediaContainers { - if ((mediaContainer.mediaAttachment?.description) == nil) || - mediaContainer.mediaAttachment?.description?.count == 0 + if ((mediaContainer.mediaAttachment?.description) == nil) + || mediaContainer.mediaAttachment?.description?.count == 0 { everyMediaHasAltText = false } @@ -186,9 +187,9 @@ public extension StatusEditor { func evaluateLanguages() { if let detectedLang = detectLanguage(text: statusText.string), - let selectedLanguage, - selectedLanguage != "", - selectedLanguage != detectedLang + let selectedLanguage, + selectedLanguage != "", + selectedLanguage != detectedLang { languageConfirmationDialogLanguages = (detected: detectedLang, selected: selectedLanguage) } else { @@ -220,18 +221,20 @@ public extension StatusEditor { let postStatus: Status? var pollData: StatusData.PollData? if let pollOptions = getPollOptionsForAPI() { - pollData = .init(options: pollOptions, - multiple: pollVotingFrequency.canVoteMultipleTimes, - expires_in: pollDuration.rawValue) + pollData = .init( + options: pollOptions, + multiple: pollVotingFrequency.canVoteMultipleTimes, + expires_in: pollDuration.rawValue) } - let data = StatusData(status: statusText.string, - visibility: visibility, - inReplyToId: mode.replyToStatus?.id, - spoilerText: spoilerOn ? spoilerText : nil, - mediaIds: mediaContainers.compactMap { $0.mediaAttachment?.id }, - poll: pollData, - language: selectedLanguage, - mediaAttributes: mediaAttributes) + let data = StatusData( + status: statusText.string, + visibility: visibility, + inReplyToId: mode.replyToStatus?.id, + spoilerText: spoilerOn ? spoilerText : nil, + mediaIds: mediaContainers.compactMap { $0.mediaAttachment?.id }, + poll: pollData, + language: selectedLanguage, + mediaAttributes: mediaAttributes) switch mode { case .new, .replyTo, .quote, .mention, .shareExtension, .quoteLink, .imageURL: postStatus = try await client.post(endpoint: Statuses.postStatus(json: data)) @@ -239,7 +242,8 @@ public extension StatusEditor { StreamWatcher.shared.emmitPostEvent(for: postStatus) } case let .edit(status): - postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id, json: data)) + postStatus = try await client.put( + endpoint: Statuses.editStatus(id: status.id, json: data)) if let postStatus { StreamWatcher.shared.emmitEditEvent(for: postStatus) } @@ -349,7 +353,8 @@ public extension StatusEditor { case let .edit(status): var rawText = status.content.asRawText.escape() for mention in status.mentions { - rawText = rawText.replacingOccurrences(of: "@\(mention.username)", with: "@\(mention.acct)") + rawText = rawText.replacingOccurrences( + of: "@\(mention.username)", with: "@\(mention.acct)") } statusText = .init(string: rawText) selectedRange = .init(location: statusText.string.utf16.count, length: 0) @@ -369,7 +374,8 @@ public extension StatusEditor { case let .quote(status): embeddedStatus = status if let url = embeddedStatusURL { - statusText = .init(string: "\n\nFrom: @\(status.reblog?.account.acct ?? status.account.acct)\n\(url)") + statusText = .init( + string: "\n\nFrom: @\(status.reblog?.account.acct ?? status.account.acct)\n\(url)") selectedRange = .init(location: 0, length: 0) } case let .quoteLink(link): @@ -380,11 +386,14 @@ public extension StatusEditor { private func processText() { guard markedTextRange == nil else { return } - statusText.addAttributes([.foregroundColor: UIColor(Theme.shared.labelColor), - .font: Font.scaledBodyUIFont, - .backgroundColor: UIColor.clear, - .underlineColor: UIColor.clear], - range: NSMakeRange(0, statusText.string.utf16.count)) + statusText.addAttributes( + [ + .foregroundColor: UIColor(Theme.shared.labelColor), + .font: Font.scaledBodyUIFont, + .backgroundColor: UIColor.clear, + .underlineColor: UIColor.clear, + ], + range: NSMakeRange(0, statusText.string.utf16.count)) let hashtagPattern = "(#+[\\w0-9(_)]{0,})" let mentionPattern = "(@+[a-zA-Z0-9(_).-]{1,})" let urlPattern = "(?i)https?://(?:www\\.)?\\S+(?:/|\\b)" @@ -395,23 +404,31 @@ public extension StatusEditor { let urlRegex = try NSRegularExpression(pattern: urlPattern, options: []) let range = NSMakeRange(0, statusText.string.utf16.count) - var ranges = hashtagRegex.matches(in: statusText.string, - options: [], - range: range).map(\.range) - ranges.append(contentsOf: mentionRegex.matches(in: statusText.string, - options: [], - range: range).map(\.range)) + var ranges = hashtagRegex.matches( + in: statusText.string, + options: [], + range: range + ).map(\.range) + ranges.append( + contentsOf: mentionRegex.matches( + in: statusText.string, + options: [], + range: range + ).map(\.range)) - let urlRanges = urlRegex.matches(in: statusText.string, - options: [], - range: range).map(\.range) + let urlRanges = urlRegex.matches( + in: statusText.string, + options: [], + range: range + ).map(\.range) var foundSuggestionRange = false for nsRange in ranges { - statusText.addAttributes([.foregroundColor: UIColor(theme?.tintColor ?? .brand)], - range: nsRange) + statusText.addAttributes( + [.foregroundColor: UIColor(theme?.tintColor ?? .brand)], + range: nsRange) if selectedRange.location == (nsRange.location + nsRange.length), - let range = Range(nsRange, in: statusText.string) + let range = Range(nsRange, in: statusText.string) { foundSuggestionRange = true currentSuggestionRange = nsRange @@ -430,10 +447,13 @@ public extension StatusEditor { numUrls += 1 totalUrlLength += range.length - statusText.addAttributes([.foregroundColor: UIColor(theme?.tintColor ?? .brand), - .underlineStyle: NSUnderlineStyle.single.rawValue, - .underlineColor: UIColor(theme?.tintColor ?? .brand)], - range: NSRange(location: range.location, length: range.length)) + statusText.addAttributes( + [ + .foregroundColor: UIColor(theme?.tintColor ?? .brand), + .underlineStyle: NSUnderlineStyle.single.rawValue, + .underlineColor: UIColor(theme?.tintColor ?? .brand), + ], + range: NSRange(location: range.location, length: range.length)) } urlLengthAdjustments = totalUrlLength - (maxLengthOfUrl * numUrls) @@ -459,12 +479,13 @@ public extension StatusEditor { isMediasLoading = true let url = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).gif") try? data.write(to: url) - let container = MediaContainer(id: UUID().uuidString, - image: nil, - movieTransferable: nil, - gifTransferable: .init(url: url), - mediaAttachment: nil, - error: nil) + let container = MediaContainer( + id: UUID().uuidString, + image: nil, + movieTransferable: nil, + gifTransferable: .init(url: url), + mediaAttachment: nil, + error: nil) prepareToPost(for: container) } @@ -502,8 +523,8 @@ public extension StatusEditor { ) prepareToPost(for: container) } else if let content = content as? ImageFileTranseferable, - let compressedData = await compressor.compressImageFrom(url: content.url), - let image = UIImage(data: compressedData) + let compressedData = await compressor.compressImageFrom(url: content.url), + let image = UIImage(data: compressedData) { let container = MediaContainer( id: UUID().uuidString, @@ -564,7 +585,7 @@ public extension StatusEditor { private func checkEmbed() { if let url = embeddedStatusURL, - !statusText.string.contains(url.absoluteString) + !statusText.string.contains(url.absoluteString) { embeddedStatus = nil mode = .new(text: nil, visibility: visibility) @@ -590,11 +611,13 @@ public extension StatusEditor { } showRecentsTagsInline = false query.removeFirst() - results = try await client.get(endpoint: Search.search(query: query, - type: .hashtags, - offset: 0, - following: nil), - forceVersion: .v2) + results = try await client.get( + endpoint: Search.search( + query: query, + type: .hashtags, + offset: 0, + following: nil), + forceVersion: .v2) guard !Task.isCancelled else { return } @@ -604,11 +627,13 @@ public extension StatusEditor { case "@": guard query.utf8.count > 1 else { return } query.removeFirst() - let accounts: [Account] = try await client.get(endpoint: Search.accountsSearch(query: query, - type: nil, - offset: 0, - following: nil), - forceVersion: .v1) + let accounts: [Account] = try await client.get( + endpoint: Search.accountsSearch( + query: query, + type: nil, + offset: 0, + following: nil), + forceVersion: .v1) guard !Task.isCancelled else { return } @@ -623,10 +648,8 @@ public extension StatusEditor { } private func resetAutoCompletion() { - if !tagsSuggestions.isEmpty || - !mentionsSuggestions.isEmpty || - currentSuggestionRange != nil || - showRecentsTagsInline + if !tagsSuggestions.isEmpty || !mentionsSuggestions.isEmpty || currentSuggestionRange != nil + || showRecentsTagsInline { withAnimation { tagsSuggestions = [] @@ -684,7 +707,8 @@ public extension StatusEditor { } } - nonisolated func makeMediaContainer(from pickerItem: PhotosPickerItem) async -> MediaContainer? { + nonisolated func makeMediaContainer(from pickerItem: PhotosPickerItem) async -> MediaContainer? + { await withTaskGroup(of: MediaContainer?.self, returning: MediaContainer?.self) { taskGroup in taskGroup.addTask(priority: .high) { await Self.makeImageContainer(from: pickerItem) } taskGroup.addTask(priority: .high) { await Self.makeGifContainer(from: pickerItem) } @@ -701,8 +725,10 @@ public extension StatusEditor { } } - private static func makeGifContainer(from pickerItem: PhotosPickerItem) async -> MediaContainer? { - guard let gifFile = try? await pickerItem.loadTransferable(type: GifFileTranseferable.self) else { return nil } + private static func makeGifContainer(from pickerItem: PhotosPickerItem) async -> MediaContainer? + { + guard let gifFile = try? await pickerItem.loadTransferable(type: GifFileTranseferable.self) + else { return nil } return MediaContainer( id: pickerItem.itemIdentifier ?? UUID().uuidString, @@ -714,8 +740,12 @@ public extension StatusEditor { ) } - private static func makeMovieContainer(from pickerItem: PhotosPickerItem) async -> MediaContainer? { - guard let movieFile = try? await pickerItem.loadTransferable(type: MovieFileTranseferable.self) else { return nil } + private static func makeMovieContainer(from pickerItem: PhotosPickerItem) async + -> MediaContainer? + { + guard + let movieFile = try? await pickerItem.loadTransferable(type: MovieFileTranseferable.self) + else { return nil } return MediaContainer( id: pickerItem.itemIdentifier ?? UUID().uuidString, @@ -727,13 +757,17 @@ public extension StatusEditor { ) } - private static func makeImageContainer(from pickerItem: PhotosPickerItem) async -> MediaContainer? { - guard let imageFile = try? await pickerItem.loadTransferable(type: ImageFileTranseferable.self) else { return nil } + private static func makeImageContainer(from pickerItem: PhotosPickerItem) async + -> MediaContainer? + { + guard + let imageFile = try? await pickerItem.loadTransferable(type: ImageFileTranseferable.self) + else { return nil } let compressor = Compressor() guard let compressedData = await compressor.compressImageFrom(url: imageFile.url), - let image = UIImage(data: compressedData) + let image = UIImage(data: compressedData) else { return nil } return MediaContainer( @@ -753,16 +787,17 @@ public extension StatusEditor { let compressor = Compressor() _ = url.startAccessingSecurityScopedResource() 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, - movieTransferable: nil, - gifTransferable: nil, - mediaAttachment: nil, - error: nil - )) + containers.append( + MediaContainer( + id: UUID().uuidString, + image: image, + movieTransferable: nil, + gifTransferable: nil, + mediaAttachment: nil, + error: nil + )) } url.stopAccessingSecurityScopedResource() @@ -803,10 +838,11 @@ public extension StatusEditor { scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia) } } else if let videoURL = originalContainer.movieTransferable?.url, - let compressedVideoURL = await compressor.compressVideo(videoURL), - let data = try? Data(contentsOf: compressedVideoURL) + let compressedVideoURL = await compressor.compressVideo(videoURL), + let data = try? Data(contentsOf: compressedVideoURL) { - let uploadedMedia = try await uploadMedia(data: data, mimeType: compressedVideoURL.mimeType()) + let uploadedMedia = try await uploadMedia( + data: data, mimeType: compressedVideoURL.mimeType()) if let index = indexOf(container: newContainer) { mediaContainers[index] = MediaContainer( id: originalContainer.id, @@ -855,14 +891,18 @@ public extension StatusEditor { Task { repeat { if let client, - let index = mediaContainers.firstIndex(where: { $0.mediaAttachment?.id == mediaAttachement.id }) + let index = mediaContainers.firstIndex(where: { + $0.mediaAttachment?.id == mediaAttachement.id + }) { guard mediaContainers[index].mediaAttachment?.url == nil else { return } do { - let newAttachement: MediaAttachment = try await client.get(endpoint: Media.media(id: mediaAttachement.id, - json: nil)) + let newAttachement: MediaAttachment = try await client.get( + endpoint: Media.media( + id: mediaAttachement.id, + json: nil)) if newAttachement.url != nil { let oldContainer = mediaContainers[index] mediaContainers[index] = MediaContainer( @@ -885,8 +925,10 @@ public extension StatusEditor { guard let client, let attachment = container.mediaAttachment else { return } if let index = indexOf(container: container) { do { - let media: MediaAttachment = try await client.put(endpoint: Media.media(id: attachment.id, - json: .init(description: description))) + let media: MediaAttachment = try await client.put( + endpoint: Media.media( + id: attachment.id, + json: .init(description: description))) mediaContainers[index] = MediaContainer( id: container.id, image: nil, @@ -903,18 +945,21 @@ public extension StatusEditor { func editDescription(container: MediaContainer, description: String) async { guard let attachment = container.mediaAttachment else { return } if indexOf(container: container) != nil { - mediaAttributes.append(StatusData.MediaAttribute(id: attachment.id, description: description, thumbnail: nil, focus: nil)) + mediaAttributes.append( + StatusData.MediaAttribute( + id: attachment.id, description: description, thumbnail: nil, focus: nil)) } } private func uploadMedia(data: Data, mimeType: String) async throws -> MediaAttachment? { guard let client else { return nil } - return try await client.mediaUpload(endpoint: Media.medias, - version: .v2, - method: "POST", - mimeType: mimeType, - filename: "file", - data: data) + return try await client.mediaUpload( + endpoint: Media.medias, + version: .v2, + method: "POST", + mimeType: mimeType, + filename: "file", + data: data) } // MARK: - Custom emojis @@ -939,9 +984,13 @@ public extension StatusEditor { return dict }.sorted(by: { lhs, rhs in - if rhs.key == "Custom" { false } - else if lhs.key == "Custom" { true } - else { lhs.key < rhs.key } + if rhs.key == "Custom" { + false + } else if lhs.key == "Custom" { + true + } else { + lhs.key < rhs.key + } }).forEach { key, value in emojiContainers.append(.init(categoryName: key, emojis: value)) } @@ -969,9 +1018,9 @@ extension StatusEditor.ViewModel: UITextPasteDelegate { _: UITextPasteConfigurationSupporting, transform item: UITextPasteItem ) { - if !item.itemProvider.registeredContentTypes(conformingTo: .image).isEmpty || - !item.itemProvider.registeredContentTypes(conformingTo: .video).isEmpty || - !item.itemProvider.registeredContentTypes(conformingTo: .gif).isEmpty + if !item.itemProvider.registeredContentTypes(conformingTo: .image).isEmpty + || !item.itemProvider.registeredContentTypes(conformingTo: .video).isEmpty + || !item.itemProvider.registeredContentTypes(conformingTo: .gif).isEmpty { processItemsProvider(items: [item.itemProvider]) item.setNoResult() diff --git a/Packages/StatusKit/Sources/StatusKit/Embed/StatusEmbeddedView.swift b/Packages/StatusKit/Sources/StatusKit/Embed/StatusEmbeddedView.swift index 86343216..fdf8c48e 100644 --- a/Packages/StatusKit/Sources/StatusKit/Embed/StatusEmbeddedView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Embed/StatusEmbeddedView.swift @@ -23,15 +23,18 @@ public struct StatusEmbeddedView: View { HStack { VStack(alignment: .leading) { makeAccountView(account: status.reblog?.account ?? status.account) - StatusRowView(viewModel: .init(status: status, - client: client, - routerPath: routerPath, - showActions: false), - context: .timeline) - .accessibilityLabel(status.content.asRawText) - .environment(\.isCompact, true) - .environment(\.isMediaCompact, true) - .environment(\.isStatusFocused, false) + StatusRowView( + viewModel: .init( + status: status, + client: client, + routerPath: routerPath, + showActions: false), + context: .timeline + ) + .accessibilityLabel(status.content.asRawText) + .environment(\.isCompact, true) + .environment(\.isMediaCompact, true) + .environment(\.isStatusFocused, false) } Spacer() } @@ -41,13 +44,13 @@ public struct StatusEmbeddedView: View { #else .background(theme.secondaryBackgroundColor) #endif - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(.gray.opacity(0.35), lineWidth: 1) - ) - .padding(.top, 8) - .accessibilityElement(children: .combine) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(.gray.opacity(0.35), lineWidth: 1) + ) + .padding(.top, 8) + .accessibilityElement(children: .combine) } private func makeAccountView(account: Account) -> some View { @@ -60,9 +63,8 @@ public struct StatusEmbeddedView: View { .emojiText.baselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset) .fontWeight(.semibold) Group { - Text("@\(account.acct)") + - Text(" ⸱ ") + - Text(status.reblog?.createdAt.relativeFormatted ?? status.createdAt.relativeFormatted) + Text("@\(account.acct)") + Text(" ⸱ ") + + Text(status.reblog?.createdAt.relativeFormatted ?? status.createdAt.relativeFormatted) } .font(.scaledCaption) .foregroundStyle(.secondary) diff --git a/Packages/StatusKit/Sources/StatusKit/Ext/Visibility.swift b/Packages/StatusKit/Sources/StatusKit/Ext/Visibility.swift index e6abcd3b..73de32aa 100644 --- a/Packages/StatusKit/Sources/StatusKit/Ext/Visibility.swift +++ b/Packages/StatusKit/Sources/StatusKit/Ext/Visibility.swift @@ -1,12 +1,12 @@ import Models import SwiftUI -public extension Models.Visibility { - static var supportDefault: [Self] { +extension Models.Visibility { + public static var supportDefault: [Self] { [.pub, .priv, .unlisted] } - var iconName: String { + public var iconName: String { switch self { case .pub: "globe.americas" @@ -19,7 +19,7 @@ public extension Models.Visibility { } } - var title: LocalizedStringKey { + public var title: LocalizedStringKey { switch self { case .pub: "status.visibility.public" diff --git a/Packages/StatusKit/Sources/StatusKit/History/StatusEditHistoryView.swift b/Packages/StatusKit/Sources/StatusKit/History/StatusEditHistoryView.swift index 2dce38e2..ea9911cd 100644 --- a/Packages/StatusKit/Sources/StatusKit/History/StatusEditHistoryView.swift +++ b/Packages/StatusKit/Sources/StatusKit/History/StatusEditHistoryView.swift @@ -29,9 +29,8 @@ public struct StatusEditHistoryView: View { .emojiText.size(Font.scaledBodyFont.emojiSize) .emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset) Group { - Text(edit.createdAt.asDate, style: .date) + - Text("status.summary.at-time") + - Text(edit.createdAt.asDate, style: .time) + Text(edit.createdAt.asDate, style: .date) + Text("status.summary.at-time") + + Text(edit.createdAt.asDate, style: .time) } .font(.footnote) .foregroundStyle(.secondary) diff --git a/Packages/StatusKit/Sources/StatusKit/List/StatusesListView.swift b/Packages/StatusKit/Sources/StatusKit/List/StatusesListView.swift index ac31f86e..275af7d0 100644 --- a/Packages/StatusKit/Sources/StatusKit/List/StatusesListView.swift +++ b/Packages/StatusKit/Sources/StatusKit/List/StatusesListView.swift @@ -14,11 +14,12 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { private let routerPath: RouterPath private let client: Client - public init(fetcher: Fetcher, - client: Client, - routerPath: RouterPath, - isRemote: Bool = false) - { + public init( + fetcher: Fetcher, + client: Client, + routerPath: RouterPath, + isRemote: Bool = false + ) { _fetcher = .init(initialValue: fetcher) self.isRemote = isRemote self.client = client @@ -29,16 +30,19 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { switch fetcher.statusesState { case .loading: ForEach(Status.placeholders()) { status in - StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath), - context: .timeline) - .redacted(reason: .placeholder) - .allowsHitTesting(false) + StatusRowView( + viewModel: .init(status: status, client: client, routerPath: routerPath), + context: .timeline + ) + .redacted(reason: .placeholder) + .allowsHitTesting(false) } case .error: - ErrorView(title: "status.error.title", - message: "status.error.loading.message", - buttonTitle: "action.retry") - { + ErrorView( + title: "status.error.title", + message: "status.error.loading.message", + buttonTitle: "action.retry" + ) { await fetcher.fetchNewestStatuses(pullToRefresh: false) } .listRowBackground(theme.primaryBackgroundColor) @@ -46,17 +50,20 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { case let .display(statuses, nextPageState): ForEach(statuses) { status in - StatusRowView(viewModel: StatusRowViewModel(status: status, - client: client, - routerPath: routerPath, - isRemote: isRemote), - context: .timeline) - .onAppear { - fetcher.statusDidAppear(status: status) - } - .onDisappear { - fetcher.statusDidDisappear(status: status) - } + StatusRowView( + viewModel: StatusRowViewModel( + status: status, + client: client, + routerPath: routerPath, + isRemote: isRemote), + context: .timeline + ) + .onAppear { + fetcher.statusDidAppear(status: status) + } + .onDisappear { + fetcher.statusDidDisappear(status: status) + } } switch nextPageState { case .hasNextPage: @@ -65,7 +72,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { } .padding(.horizontal, .layoutPadding) #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) #endif case .none: diff --git a/Packages/StatusKit/Sources/StatusKit/Poll/StatusPollView.swift b/Packages/StatusKit/Sources/StatusKit/Poll/StatusPollView.swift index b2379864..7b11743f 100644 --- a/Packages/StatusKit/Sources/StatusKit/Poll/StatusPollView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Poll/StatusPollView.swift @@ -35,7 +35,7 @@ public struct StatusPollView: View { private func isSelected(option: Poll.Option) -> Bool { if let optionIndex = viewModel.poll.options.firstIndex(where: { $0.id == option.id }), - let _ = viewModel.votes.firstIndex(of: optionIndex) + viewModel.votes.firstIndex(of: optionIndex) != nil { return true } @@ -113,21 +113,25 @@ public struct StatusPollView: View { Task { await viewModel.fetchPoll() } } .accessibilityElement(children: .contain) - .accessibilityLabel(viewModel.poll.expired ? "accessibility.status.poll.finished.label" : "accessibility.status.poll.active.label") + .accessibilityLabel( + viewModel.poll.expired + ? "accessibility.status.poll.finished.label" : "accessibility.status.poll.active.label") } func combinedAccessibilityLabel(for option: Poll.Option, index: Int) -> Text { let showPercentage = viewModel.poll.expired || viewModel.poll.voted ?? false - return Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(viewModel.poll.options.count)") + - Text(", ") + - Text(option.title) + - Text(showPercentage ? ", \(absolutePercent(for: option.votesCount ?? 0))%" : "") + return Text( + "accessibility.status.poll.option-prefix-\(index + 1)-of-\(viewModel.poll.options.count)") + + Text(", ") + Text(option.title) + + Text(showPercentage ? ", \(absolutePercent(for: option.votesCount ?? 0))%" : "") } private var footerView: some View { HStack(spacing: 0) { if viewModel.poll.multiple { - Text("status.poll.n-votes-voters \(viewModel.poll.votesCount) \(viewModel.poll.safeVotersCount)") + Text( + "status.poll.n-votes-voters \(viewModel.poll.votesCount) \(viewModel.poll.safeVotersCount)" + ) } else { Text("status.poll.n-votes \(viewModel.poll.votesCount)") } @@ -149,7 +153,7 @@ public struct StatusPollView: View { private func makeBarView(for option: Poll.Option, buttonImage: some View) -> some View { Button { if !viewModel.poll.expired, - let index = viewModel.poll.options.firstIndex(where: { $0.id == option.id }) + let index = viewModel.poll.options.firstIndex(where: { $0.id == option.id }) { withAnimation { viewModel.handleSelection(index) @@ -173,8 +177,10 @@ public struct StatusPollView: View { if viewModel.showResults || status.account.id == currentAccount.account?.id { _PercentWidthLayout(percent: relativePercent(for: option.votesCount ?? 0)) { RoundedRectangle(cornerRadius: 10).foregroundColor(theme.tintColor) - .transition(.asymmetric(insertion: .push(from: .leading), - removal: .push(from: .trailing))) + .transition( + .asymmetric( + insertion: .push(from: .leading), + removal: .push(from: .trailing))) } } } @@ -193,9 +199,11 @@ public struct StatusPollView: View { return view.sizeThatFits(proposal) } - func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) { + func placeSubviews( + in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout () + ) { guard let view = subviews.first, - let width = proposal.width + let width = proposal.width else { return } view.place( diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift index 92dab792..8c65754f 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift @@ -99,13 +99,13 @@ struct StatusActionButtonStyle: ButtonStyle { static func generateCells() -> [Cell] { let cellCount = 16 - let velocityRange = 0.6 ... 1.0 - let scaleRange = 0.5 ... 1.0 - let alphaRange = 0.6 ... 1.0 + let velocityRange = 0.6...1.0 + let scaleRange = 0.5...1.0 + let alphaRange = 0.6...1.0 let spacing = 2 * .pi / (1.618 + Double(arc4random() % 200) / 10000) let initialSpacing = deg2rad(Double(arc4random() % 360)) - return (0 ..< cellCount).map { index in + return (0.. 1 { - return Text("accessibility.image.alt-text-\(descriptions[0])") + Text(", ") + Text("accessibility.image.alt-text-more.label") + Text(", ") + return Text("accessibility.image.alt-text-\(descriptions[0])") + Text(", ") + + Text("accessibility.image.alt-text-more.label") + Text(", ") } else if viewModel.finalStatus.mediaAttachments.isEmpty == false { - let differentTypes = Set(viewModel.finalStatus.mediaAttachments.compactMap(\.localizedTypeDescription)).sorted() - return Text("accessibility.status.contains-media.label-\(ListFormatter.localizedString(byJoining: differentTypes))") + Text(", ") + let differentTypes = Set( + viewModel.finalStatus.mediaAttachments.compactMap(\.localizedTypeDescription) + ).sorted() + return Text( + "accessibility.status.contains-media.label-\(ListFormatter.localizedString(byJoining: differentTypes))" + ) + Text(", ") } else { return Text("") } @@ -97,24 +105,23 @@ struct StatusRowAccessibilityLabel { func pollText() -> Text { if let poll = viewModel.finalStatus.poll { let showPercentage = poll.expired || poll.voted ?? false - let title: LocalizedStringKey = poll.expired + let title: LocalizedStringKey = + poll.expired ? "accessibility.status.poll.finished.label" : "accessibility.status.poll.active.label" return poll.options.enumerated().reduce(into: Text(title)) { text, pair in let (index, option) = pair let selected = poll.ownVotes?.contains(index) ?? false - let percentage = poll.safeVotersCount > 0 && option.votesCount != nil + let percentage = + poll.safeVotersCount > 0 && option.votesCount != nil ? Int(round(Double(option.votesCount!) / Double(poll.safeVotersCount) * 100)) : 0 - text = text + - Text(selected ? "accessibility.status.poll.selected.label" : "") + - Text(", ") + - Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(poll.options.count)") + - Text(", ") + - Text(option.title) + - Text(showPercentage ? ", \(percentage)%. " : ". ") + text = + text + Text(selected ? "accessibility.status.poll.selected.label" : "") + Text(", ") + + Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(poll.options.count)") + + Text(", ") + Text(option.title) + Text(showPercentage ? ", \(percentage)%. " : ". ") } } return Text("") diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift index 76164b14..3bfb7784 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift @@ -32,17 +32,18 @@ public struct StatusRowView: View { public let context: Context var contextMenu: some View { - StatusRowContextMenu(viewModel: viewModel, - showTextForSelection: $showSelectableText, - isBlockConfirmationPresented: $isBlockConfirmationPresented, - isShareAsImageSheetPresented: $isShareAsImageSheetPresented) + StatusRowContextMenu( + viewModel: viewModel, + showTextForSelection: $showSelectableText, + isBlockConfirmationPresented: $isBlockConfirmationPresented, + isShareAsImageSheetPresented: $isShareAsImageSheetPresented) } public var body: some View { HStack(spacing: 0) { if !isCompact { HStack(spacing: 3) { - ForEach(0 ..< indentationLevel, id: \.self) { level in + ForEach(0.. collapseThresholdLength let newlineLimit = showCollapseButton && isCollapsed ? collapsedLines : nil if newlineLimit != lineLimit { @@ -102,8 +103,8 @@ import SwiftUI } var isThread: Bool { - status.reblog?.inReplyToId != nil || status.reblog?.inReplyToAccountId != nil || - status.inReplyToId != nil || status.inReplyToAccountId != nil + status.reblog?.inReplyToId != nil || status.reblog?.inReplyToAccountId != nil + || status.inReplyToId != nil || status.inReplyToAccountId != nil } var url: URL? { @@ -152,25 +153,27 @@ import SwiftUI } func makeDecorativeGradient(startColor: Color, endColor: Color) -> some View { - LinearGradient(stops: [ - .init(color: startColor.opacity(0.2), location: 0.03), - .init(color: startColor.opacity(0.1), location: 0.06), - .init(color: startColor.opacity(0.05), location: 0.09), - .init(color: startColor.opacity(0.02), location: 0.15), - .init(color: endColor, location: 0.25), - ], - startPoint: .topLeading, - endPoint: .bottomTrailing) + LinearGradient( + stops: [ + .init(color: startColor.opacity(0.2), location: 0.03), + .init(color: startColor.opacity(0.1), location: 0.06), + .init(color: startColor.opacity(0.05), location: 0.09), + .init(color: startColor.opacity(0.02), location: 0.15), + .init(color: endColor, location: 0.25), + ], + startPoint: .topLeading, + endPoint: .bottomTrailing) } - public init(status: Status, - client: Client, - routerPath: RouterPath, - isRemote: Bool = false, - showActions: Bool = true, - textDisabled: Bool = false, - scrollToId: Binding? = nil) - { + public init( + status: Status, + client: Client, + routerPath: RouterPath, + isRemote: Bool = false, + showActions: Bool = true, + textDisabled: Bool = false, + scrollToId: Binding? = nil + ) { self.status = status finalStatus = status.reblog ?? status self.client = client @@ -197,13 +200,16 @@ import SwiftUI } userFollowedTag = finalStatus.content.links.first(where: { link in - link.type == .hashtag && CurrentAccount.shared.tags.contains(where: { $0.name.lowercased() == link.title.lowercased() }) + link.type == .hashtag + && CurrentAccount.shared.tags.contains(where: { + $0.name.lowercased() == link.title.lowercased() + }) }) isFiltered = filter != nil if let url = embededStatusURL(), - let embed = StatusEmbedCache.shared.get(url: url) + let embed = StatusEmbedCache.shared.get(url: url) { isEmbedLoading = false embeddedStatus = embed @@ -237,7 +243,7 @@ import SwiftUI func goToParent() { guard let id = status.inReplyToId else { return } - if let _ = scrollToId { + if scrollToId != nil { scrollToId?.wrappedValue = id } else { routerPath.navigate(to: .statusDetail(id: id)) @@ -245,16 +251,17 @@ import SwiftUI } func loadAuthorRelationship() async { - let relationships: [Relationship]? = try? await client.get(endpoint: Accounts.relationships(ids: [status.reblog?.account.id ?? status.account.id])) + let relationships: [Relationship]? = try? await client.get( + endpoint: Accounts.relationships(ids: [status.reblog?.account.id ?? status.account.id])) authorRelationship = relationships?.first } private func embededStatusURL() -> URL? { let content = finalStatus.content if !content.statusesURLs.isEmpty, - let url = content.statusesURLs.first, - !StatusEmbedCache.shared.badStatusesURLs.contains(url), - client.hasConnection(with: url) + let url = content.statusesURLs.first, + !StatusEmbedCache.shared.badStatusesURLs.contains(url), + client.hasConnection(with: url) { return url } @@ -263,7 +270,7 @@ import SwiftUI func loadEmbeddedStatus() async { guard embeddedStatus == nil, - let url = embededStatusURL() + let url = embededStatusURL() else { if isEmbedLoading { isEmbedLoading = false @@ -283,11 +290,13 @@ import SwiftUI if url.absoluteString.contains(client.server), let id = Int(url.lastPathComponent) { embed = try await client.get(endpoint: Statuses.status(id: String(id))) } else { - let results: SearchResults = try await client.get(endpoint: Search.search(query: url.absoluteString, - type: .statuses, - offset: 0, - following: nil), - forceVersion: .v2) + let results: SearchResults = try await client.get( + endpoint: Search.search( + query: url.absoluteString, + type: .statuses, + offset: 0, + following: nil), + forceVersion: .v2) embed = results.statuses.first } if let embed { @@ -340,8 +349,10 @@ import SwiftUI withAnimation(.smooth) { actionsAccountsFetched = true } - let favoriters: [Account] = try await client.get(endpoint: Statuses.favoritedBy(id: status.id, maxId: nil)) - let rebloggers: [Account] = try await client.get(endpoint: Statuses.rebloggedBy(id: status.id, maxId: nil)) + let favoriters: [Account] = try await client.get( + endpoint: Statuses.favoritedBy(id: status.id, maxId: nil)) + let rebloggers: [Account] = try await client.get( + endpoint: Statuses.rebloggedBy(id: status.id, maxId: nil)) withAnimation(.smooth) { self.favoriters = favoriters self.rebloggers = rebloggers @@ -385,7 +396,7 @@ import SwiftUI var hasShown = false #if canImport(_Translation_SwiftUI) if translation == nil, - #available(iOS 17.4, *) + #available(iOS 17.4, *) { showAppleTranslation = true hasShown = true @@ -393,7 +404,7 @@ import SwiftUI #endif if !hasShown, - translation == nil + translation == nil { if preferredTranslationType == .useDeepl { deeplTranslationError = true @@ -409,8 +420,9 @@ import SwiftUI } let deepLClient = getDeepLClient() - let translation = try? await deepLClient.request(target: userLang, - text: finalStatus.content.asRawText) + let translation = try? await deepLClient.request( + target: userLang, + text: finalStatus.content.asRawText) withAnimation { self.translation = translation isLoadingTranslation = false @@ -422,8 +434,10 @@ import SwiftUI isLoadingTranslation = true } - let translation: Translation? = try? await client.post(endpoint: Statuses.translate(id: finalStatus.id, - lang: userLang)) + let translation: Translation? = try? await client.post( + endpoint: Statuses.translate( + id: finalStatus.id, + lang: userLang)) withAnimation { self.translation = translation @@ -454,11 +468,13 @@ import SwiftUI func fetchRemoteStatus() async -> Bool { guard isRemote, let remoteStatusURL = URL(string: finalStatus.url ?? "") else { return false } isLoadingRemoteContent = true - let results: SearchResults? = try? await client.get(endpoint: Search.search(query: remoteStatusURL.absoluteString, - type: .statuses, - offset: nil, - following: nil), - forceVersion: .v2) + let results: SearchResults? = try? await client.get( + endpoint: Search.search( + query: remoteStatusURL.absoluteString, + type: .statuses, + offset: nil, + following: nil), + forceVersion: .v2) if let status = results?.statuses.first { localStatusId = status.id localStatus = status diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowActionsView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowActionsView.swift index c267f7e9..f961d665 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowActionsView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowActionsView.swift @@ -25,11 +25,14 @@ struct StatusRowActionsView: View { var viewModel: StatusRowViewModel var isNarrow: Bool { - horizontalSizeClass == .compact && (UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac) + horizontalSizeClass == .compact + && (UIDevice.current.userInterfaceIdiom == .pad + || UIDevice.current.userInterfaceIdiom == .mac) } func privateBoost() -> Bool { - viewModel.status.visibility == .priv && viewModel.status.account.id == currentAccount.account?.id + viewModel.status.visibility == .priv + && viewModel.status.account.id == currentAccount.account?.id } var actions: [Action] { @@ -69,7 +72,9 @@ struct StatusRowActionsView: View { } } - func accessibilityLabel(dataController: StatusDataController, privateBoost: Bool = false) -> LocalizedStringKey { + func accessibilityLabel(dataController: StatusDataController, privateBoost: Bool = false) + -> LocalizedStringKey + { switch self { case .respond: return "status.action.reply" @@ -140,7 +145,7 @@ struct StatusRowActionsView: View { ForEach(actions, id: \.self) { action in if action == .share { if let urlString = viewModel.finalStatus.url, - let url = URL(string: urlString) + let url = URL(string: urlString) { switch userPreferences.shareButtonBehavior { case .linkOnly: @@ -150,56 +155,59 @@ struct StatusRowActionsView: View { .padding(.vertical, 6) .padding(.horizontal, 8) .contentShape(Rectangle()) - #if targetEnvironment(macCatalyst) - .font(.scaledBody) - #else - .font(.body) - .dynamicTypeSize(.large) - #endif + #if targetEnvironment(macCatalyst) + .font(.scaledBody) + #else + .font(.body) + .dynamicTypeSize(.large) + #endif } .buttonStyle(.borderless) #if !os(visionOS) .offset(x: -8) #endif - .accessibilityElement(children: .combine) - .accessibilityLabel("status.action.share-link") + .accessibilityElement(children: .combine) + .accessibilityLabel("status.action.share-link") case .linkAndText: - ShareLink(item: url, - subject: Text(viewModel.finalStatus.account.safeDisplayName), - message: Text(viewModel.finalStatus.content.asRawText)) - { + ShareLink( + item: url, + subject: Text(viewModel.finalStatus.account.safeDisplayName), + message: Text(viewModel.finalStatus.content.asRawText) + ) { action.image(dataController: statusDataController) .foregroundColor(Color(UIColor.secondaryLabel)) .padding(.vertical, 6) .padding(.horizontal, 8) .contentShape(Rectangle()) - #if targetEnvironment(macCatalyst) - .font(.scaledBody) - #else - .font(.body) - .dynamicTypeSize(.large) - #endif + #if targetEnvironment(macCatalyst) + .font(.scaledBody) + #else + .font(.body) + .dynamicTypeSize(.large) + #endif } .buttonStyle(.borderless) #if !os(visionOS) .offset(x: -8) #endif - .accessibilityElement(children: .combine) - .accessibilityLabel("status.action.share-link") + .accessibilityElement(children: .combine) + .accessibilityLabel("status.action.share-link") } } Spacer() } else if action == .menu { Menu { - StatusRowContextMenu(viewModel: viewModel, - showTextForSelection: $showTextForSelection, - isBlockConfirmationPresented: $isBlockConfirmationPresented, - isShareAsImageSheetPresented: $isShareAsImageSheetPresented) - .onAppear { - Task { - await viewModel.loadAuthorRelationship() - } + StatusRowContextMenu( + viewModel: viewModel, + showTextForSelection: $showTextForSelection, + isBlockConfirmationPresented: $isBlockConfirmationPresented, + isShareAsImageSheetPresented: $isShareAsImageSheetPresented + ) + .onAppear { + Task { + await viewModel.loadAuthorRelationship() } + } } label: { Label("", systemImage: "ellipsis") .padding(.vertical, 6) @@ -218,16 +226,18 @@ struct StatusRowActionsView: View { } .fixedSize(horizontal: false, vertical: true) .sheet(isPresented: $showTextForSelection) { - let content = viewModel.status.reblog?.content.asSafeMarkdownAttributedString ?? viewModel.status.content.asSafeMarkdownAttributedString + let content = + viewModel.status.reblog?.content.asSafeMarkdownAttributedString + ?? viewModel.status.content.asSafeMarkdownAttributedString StatusRowSelectableTextView(content: content) .tint(theme.tintColor) } .sheet(isPresented: $isShareAsImageSheetPresented) { let view = - HStack { - StatusRowView(viewModel: viewModel, context: .timeline) - .padding(8) - } + HStack { + StatusRowView(viewModel: viewModel, context: .timeline) + .padding(8) + } .environment(\.isInCaptureMode, true) .environment(RouterPath()) .environment(QuickLook.shared) @@ -246,8 +256,10 @@ struct StatusRowActionsView: View { let renderer = ImageRenderer(content: AnyView(view)) renderer.isOpaque = true renderer.scale = 3.0 - return StatusRowShareAsImageView(viewModel: viewModel, - renderer: renderer) + return StatusRowShareAsImageView( + viewModel: viewModel, + renderer: renderer + ) .tint(theme.tintColor) } } @@ -262,38 +274,39 @@ struct StatusRowActionsView: View { .image(dataController: statusDataController, privateBoost: privateBoost()) .imageScale(.medium) .fontWeight(.black) - #if targetEnvironment(macCatalyst) - .font(.scaledBody) - #else - .font(.body) - .dynamicTypeSize(.large) - #endif + #if targetEnvironment(macCatalyst) + .font(.scaledBody) + #else + .font(.body) + .dynamicTypeSize(.large) + #endif } else { action .image(dataController: statusDataController, privateBoost: privateBoost()) - #if targetEnvironment(macCatalyst) - .font(.scaledBody) - #else - .font(.body) - .dynamicTypeSize(.large) - #endif + #if targetEnvironment(macCatalyst) + .font(.scaledBody) + #else + .font(.body) + .dynamicTypeSize(.large) + #endif } if !isNarrow, - let count = action.count(dataController: statusDataController, - isFocused: isFocused, - theme: theme), !viewModel.isRemote + let count = action.count( + dataController: statusDataController, + isFocused: isFocused, + theme: theme), !viewModel.isRemote { Text(count, format: .number.notation(.compactName)) .lineLimit(1) .minimumScaleFactor(0.6) .contentTransition(.numericText(value: Double(count))) .foregroundColor(Color(UIColor.secondaryLabel)) - #if targetEnvironment(macCatalyst) - .font(.scaledFootnote) - #else - .font(.footnote) - .dynamicTypeSize(.medium) - #endif + #if targetEnvironment(macCatalyst) + .font(.scaledFootnote) + #else + .font(.footnote) + .dynamicTypeSize(.medium) + #endif .monospacedDigit() .opacity(count > 0 ? 1 : 0) } @@ -303,21 +316,26 @@ struct StatusRowActionsView: View { .contentShape(Rectangle()) } #if os(visionOS) - .buttonStyle(.borderless) - .foregroundColor(Color(UIColor.secondaryLabel)) + .buttonStyle(.borderless) + .foregroundColor(Color(UIColor.secondaryLabel)) #else - .buttonStyle( - .statusAction( - isOn: action.isOn(dataController: statusDataController), - tintColor: action.tintColor(theme: theme) + .buttonStyle( + .statusAction( + isOn: action.isOn(dataController: statusDataController), + tintColor: action.tintColor(theme: theme) + ) ) - ) - .offset(x: -8) + .offset(x: -8) #endif - .disabled(action == .boost && - (viewModel.status.visibility == .direct || viewModel.status.visibility == .priv && viewModel.status.account.id != currentAccount.account?.id)) + .disabled( + action == .boost + && (viewModel.status.visibility == .direct + || viewModel.status.visibility == .priv + && viewModel.status.account.id != currentAccount.account?.id) + ) .accessibilityElement(children: .combine) - .accessibilityLabel(action.accessibilityLabel(dataController: statusDataController, privateBoost: privateBoost())) + .accessibilityLabel( + action.accessibilityLabel(dataController: statusDataController, privateBoost: privateBoost())) } private func handleAction(action: Action) { @@ -332,9 +350,12 @@ struct StatusRowActionsView: View { case .respond: SoundEffectManager.shared.playSound(.share) #if targetEnvironment(macCatalyst) || os(visionOS) - openWindow(value: WindowDestinationEditor.replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status)) + openWindow( + value: WindowDestinationEditor.replyToStatusEditor( + status: viewModel.localStatus ?? viewModel.status)) #else - viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status) + viewModel.routerPath.presentedSheet = .replyToStatusEditor( + status: viewModel.localStatus ?? viewModel.status) #endif case .favorite: SoundEffectManager.shared.playSound(.favorite) diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowCardView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowCardView.swift index de19bbb0..b6bd8e2f 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowCardView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowCardView.swift @@ -50,12 +50,14 @@ public struct StatusRowCardView: View { } label: { if let title = card.title, let url = URL(string: card.url) { VStack(alignment: .leading, spacing: 0) { - let sitesWithIcons = ["apps.apple.com", "music.apple.com", "podcasts.apple.com", "open.spotify.com"] + let sitesWithIcons = [ + "apps.apple.com", "music.apple.com", "podcasts.apple.com", "open.spotify.com", + ] if isCompact { compactLinkPreview(title, url) - } else if UIDevice.current.userInterfaceIdiom == .pad || - UIDevice.current.userInterfaceIdiom == .mac || - UIDevice.current.userInterfaceIdiom == .vision, + } else if UIDevice.current.userInterfaceIdiom == .pad + || UIDevice.current.userInterfaceIdiom == .mac + || UIDevice.current.userInterfaceIdiom == .vision, let host = url.host(), sitesWithIcons.contains(host) { iconLinkPreview(title, url) @@ -66,38 +68,43 @@ public struct StatusRowCardView: View { .frame(maxWidth: maxWidth) .fixedSize(horizontal: false, vertical: true) #if os(visionOS) - .if(!isCompact, transform: { view in - view.background(.background) - }) + .if( + !isCompact, + transform: { view in + view.background(.background) + } + ) .hoverEffect() #else .background(isCompact ? .clear : theme.secondaryBackgroundColor) #endif - .cornerRadius(isCompact ? 0 : 10) - .overlay { - if !isCompact { - RoundedRectangle(cornerRadius: 10) - .stroke(.gray.opacity(0.35), lineWidth: 1) - } + .cornerRadius(isCompact ? 0 : 10) + .overlay { + if !isCompact { + RoundedRectangle(cornerRadius: 10) + .stroke(.gray.opacity(0.35), lineWidth: 1) } - .draggable(url) - .contextMenu { - ShareLink(item: url) { - Label("status.card.share", systemImage: "square.and.arrow.up") - } - Button { openURL(url) } label: { - Label("status.action.view-in-browser", systemImage: "safari") - } - Divider() - Button { - UIPasteboard.general.url = url - } label: { - Label("status.card.copy", systemImage: "doc.on.doc") - } + } + .draggable(url) + .contextMenu { + ShareLink(item: url) { + Label("status.card.share", systemImage: "square.and.arrow.up") } - .accessibilityElement(children: .combine) - .accessibilityAddTraits(.isLink) - .accessibilityRemoveTraits(.isStaticText) + Button { + openURL(url) + } label: { + Label("status.action.view-in-browser", systemImage: "safari") + } + Divider() + Button { + UIPasteboard.general.url = url + } label: { + Label("status.card.copy", systemImage: "doc.on.doc") + } + } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isLink) + .accessibilityRemoveTraits(.isStaticText) } } .buttonStyle(.plain) @@ -292,7 +299,7 @@ struct DefaultPreviewImage: View { .overlay { image.scaledToFit() } } } - .accessibilityHidden(true) // This image is decorative + .accessibilityHidden(true) // This image is decorative .clipped() } } @@ -306,7 +313,9 @@ struct DefaultPreviewImage: View { return calculateSize(proposal) } - func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) { + func placeSubviews( + in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout () + ) { guard let view = subviews.first else { return } let size = calculateSize(proposal) @@ -314,20 +323,21 @@ struct DefaultPreviewImage: View { } private func calculateSize(_ proposal: ProposedViewSize) -> CGSize { - var size = switch (proposal.width, proposal.height) { - case (nil, nil): - CGSize(width: originalWidth, height: originalWidth) - case let (nil, .some(height)): - CGSize(width: originalWidth, height: min(height, originalWidth)) - case (0, _): - CGSize.zero - case let (.some(width), _): - if originalWidth == 0 { - CGSize(width: width, height: width / 2) - } else { - CGSize(width: width, height: width / originalWidth * originalHeight) + var size = + switch (proposal.width, proposal.height) { + case (nil, nil): + CGSize(width: originalWidth, height: originalWidth) + case let (nil, .some(height)): + CGSize(width: originalWidth, height: min(height, originalWidth)) + case (0, _): + CGSize.zero + case let (.some(width), _): + if originalWidth == 0 { + CGSize(width: width, height: width / 2) + } else { + CGSize(width: width, height: width / originalWidth * originalHeight) + } } - } size.height = min(size.height, 450) return size diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContentView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContentView.swift index fdee4297..671fd60d 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContentView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContentView.swift @@ -28,29 +28,34 @@ struct StatusRowContentView: View { } if !reasons.contains(.placeholder), - !isCompact, - viewModel.isEmbedLoading || viewModel.embeddedStatus != nil + !isCompact, + viewModel.isEmbedLoading || viewModel.embeddedStatus != nil { if let embeddedStatus = viewModel.embeddedStatus { - StatusEmbeddedView(status: embeddedStatus, - client: viewModel.client, - routerPath: viewModel.routerPath) - .fixedSize(horizontal: false, vertical: true) - .transition(.opacity) + StatusEmbeddedView( + status: embeddedStatus, + client: viewModel.client, + routerPath: viewModel.routerPath + ) + .fixedSize(horizontal: false, vertical: true) + .transition(.opacity) } else { - StatusEmbeddedView(status: Status.placeholder(), - client: viewModel.client, - routerPath: viewModel.routerPath) - .fixedSize(horizontal: false, vertical: true) - .redacted(reason: .placeholder) - .transition(.opacity) + StatusEmbeddedView( + status: Status.placeholder(), + client: viewModel.client, + routerPath: viewModel.routerPath + ) + .fixedSize(horizontal: false, vertical: true) + .redacted(reason: .placeholder) + .transition(.opacity) } } if !viewModel.finalStatus.mediaAttachments.isEmpty { HStack { - StatusRowMediaPreviewView(attachments: viewModel.finalStatus.mediaAttachments, - sensitive: viewModel.finalStatus.sensitive) + StatusRowMediaPreviewView( + attachments: viewModel.finalStatus.mediaAttachments, + sensitive: viewModel.finalStatus.sensitive) if theme.statusDisplayStyle == .compact { Spacer() } @@ -60,11 +65,11 @@ struct StatusRowContentView: View { } if let card = viewModel.finalStatus.card, - !viewModel.isEmbedLoading, - !isCompact, - theme.statusDisplayStyle != .compact, - viewModel.embeddedStatus == nil, - viewModel.finalStatus.mediaAttachments.isEmpty + !viewModel.isEmbedLoading, + !isCompact, + theme.statusDisplayStyle != .compact, + viewModel.embeddedStatus == nil, + viewModel.finalStatus.mediaAttachments.isEmpty { StatusRowCardView(card: card) } diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContextMenu.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContextMenu.swift index 0652c4d5..bbf49ed2 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContextMenu.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContextMenu.swift @@ -48,28 +48,43 @@ struct StatusRowContextMenu: View { } label: { Label("status.action.reply", systemImage: "arrowshape.turn.up.left") } - Button { Task { - HapticManager.shared.fireHaptic(.notification(.success)) - SoundEffectManager.shared.playSound(.favorite) - await statusDataController.toggleFavorite(remoteStatus: nil) - } } label: { - Label(statusDataController.isFavorited ? "status.action.unfavorite" : "status.action.favorite", systemImage: statusDataController.isFavorited ? "star.fill" : "star") + Button { + Task { + HapticManager.shared.fireHaptic(.notification(.success)) + SoundEffectManager.shared.playSound(.favorite) + await statusDataController.toggleFavorite(remoteStatus: nil) + } + } label: { + Label( + statusDataController.isFavorited + ? "status.action.unfavorite" : "status.action.favorite", + systemImage: statusDataController.isFavorited ? "star.fill" : "star") } - Button { Task { - HapticManager.shared.fireHaptic(.notification(.success)) - SoundEffectManager.shared.playSound(.boost) - await statusDataController.toggleReblog(remoteStatus: nil) - } } label: { + Button { + Task { + HapticManager.shared.fireHaptic(.notification(.success)) + SoundEffectManager.shared.playSound(.boost) + await statusDataController.toggleReblog(remoteStatus: nil) + } + } label: { boostLabel } - .disabled(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv && viewModel.status.account.id != account.account?.id) - Button { Task { - SoundEffectManager.shared.playSound(.bookmark) - HapticManager.shared.fireHaptic(.notification(.success)) - await statusDataController.toggleBookmark(remoteStatus: nil) - } } label: { - Label(statusDataController.isBookmarked ? "status.action.unbookmark" : "status.action.bookmark", - systemImage: statusDataController.isBookmarked ? "bookmark.fill" : "bookmark") + .disabled( + viewModel.status.visibility == .direct + || viewModel.status.visibility == .priv + && viewModel.status.account.id != account.account?.id + ) + Button { + Task { + SoundEffectManager.shared.playSound(.bookmark) + HapticManager.shared.fireHaptic(.notification(.success)) + await statusDataController.toggleBookmark(remoteStatus: nil) + } + } label: { + Label( + statusDataController.isBookmarked + ? "status.action.unbookmark" : "status.action.bookmark", + systemImage: statusDataController.isBookmarked ? "bookmark.fill" : "bookmark") } } .controlGroupStyle(.compactMenu) @@ -89,10 +104,14 @@ struct StatusRowContextMenu: View { Menu("status.action.share-title") { if let url = viewModel.url { - ShareLink(item: url, - subject: Text(viewModel.status.reblog?.account.safeDisplayName ?? viewModel.status.account.safeDisplayName), - message: Text(viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText)) - { + ShareLink( + item: url, + subject: Text( + viewModel.status.reblog?.account.safeDisplayName + ?? viewModel.status.account.safeDisplayName), + message: Text( + viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText) + ) { Label("status.action.share", systemImage: "square.and.arrow.up") } @@ -109,13 +128,16 @@ struct StatusRowContextMenu: View { } if let url = viewModel.url { - Button { UIApplication.shared.open(url) } label: { + Button { + UIApplication.shared.open(url) + } label: { Label("status.action.view-in-browser", systemImage: "safari") } } Button { - UIPasteboard.general.string = viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText + UIPasteboard.general.string = + viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText } label: { Label("status.action.copy-text", systemImage: "doc.on.doc") } @@ -132,7 +154,9 @@ struct StatusRowContextMenu: View { Label("status.action.copy-link", systemImage: "link") } - if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier { + if let lang = preferences.serverPreferences?.postLanguage + ?? Locale.current.language.languageCode?.identifier + { Button { Task { await viewModel.translate(userLang: lang) @@ -153,22 +177,28 @@ struct StatusRowContextMenu: View { } } } label: { - Label(viewModel.isPinned ? "status.action.unpin" : "status.action.pin", systemImage: viewModel.isPinned ? "pin.fill" : "pin") + Label( + viewModel.isPinned ? "status.action.unpin" : "status.action.pin", + systemImage: viewModel.isPinned ? "pin.fill" : "pin") } if currentInstance.isEditSupported { Button { #if targetEnvironment(macCatalyst) || os(visionOS) - openWindow(value: WindowDestinationEditor.editStatusEditor(status: viewModel.status.reblogAsAsStatus ?? viewModel.status)) + openWindow( + value: WindowDestinationEditor.editStatusEditor( + status: viewModel.status.reblogAsAsStatus ?? viewModel.status)) #else - viewModel.routerPath.presentedSheet = .editStatusEditor(status: viewModel.status.reblogAsAsStatus ?? viewModel.status) + viewModel.routerPath.presentedSheet = .editStatusEditor( + status: viewModel.status.reblogAsAsStatus ?? viewModel.status) #endif } label: { Label("status.action.edit", systemImage: "pencil") } } - Button(role: .destructive, - action: { viewModel.showDeleteAlert = true }, - label: { Label("status.action.delete", systemImage: "trash") }) + Button( + role: .destructive, + action: { viewModel.showDeleteAlert = true }, + label: { Label("status.action.delete", systemImage: "trash") }) } } else { if !viewModel.isRemote { @@ -177,8 +207,10 @@ struct StatusRowContextMenu: View { Button { Task { do { - let operationAccount = viewModel.status.reblog?.account ?? viewModel.status.account - viewModel.authorRelationship = try await client.post(endpoint: Accounts.unmute(id: operationAccount.id)) + let operationAccount = + viewModel.status.reblog?.account ?? viewModel.status.account + viewModel.authorRelationship = try await client.post( + endpoint: Accounts.unmute(id: operationAccount.id)) } catch {} } } label: { @@ -190,8 +222,11 @@ struct StatusRowContextMenu: View { Button(duration.description) { Task { do { - let operationAccount = viewModel.status.reblog?.account ?? viewModel.status.account - viewModel.authorRelationship = try await client.post(endpoint: Accounts.mute(id: operationAccount.id, json: MuteData(duration: duration.rawValue))) + let operationAccount = + viewModel.status.reblog?.account ?? viewModel.status.account + viewModel.authorRelationship = try await client.post( + endpoint: Accounts.mute( + id: operationAccount.id, json: MuteData(duration: duration.rawValue))) } catch {} } } @@ -212,7 +247,8 @@ struct StatusRowContextMenu: View { } Section { Button(role: .destructive) { - viewModel.routerPath.presentedSheet = .report(status: viewModel.status.reblogAsAsStatus ?? viewModel.status) + viewModel.routerPath.presentedSheet = .report( + status: viewModel.status.reblogAsAsStatus ?? viewModel.status) } label: { Label("status.action.report", systemImage: "exclamationmark.bubble") } @@ -224,18 +260,27 @@ struct StatusRowContextMenu: View { private var accountContactMenuItems: some View { Button { #if targetEnvironment(macCatalyst) || os(visionOS) - openWindow(value: WindowDestinationEditor.mentionStatusEditor(account: viewModel.status.reblog?.account ?? viewModel.status.account, visibility: .pub)) + openWindow( + value: WindowDestinationEditor.mentionStatusEditor( + account: viewModel.status.reblog?.account ?? viewModel.status.account, visibility: .pub) + ) #else - viewModel.routerPath.presentedSheet = .mentionStatusEditor(account: viewModel.status.reblog?.account ?? viewModel.status.account, visibility: .pub) + viewModel.routerPath.presentedSheet = .mentionStatusEditor( + account: viewModel.status.reblog?.account ?? viewModel.status.account, visibility: .pub) #endif } label: { Label("status.action.mention", systemImage: "at") } Button { #if targetEnvironment(macCatalyst) || os(visionOS) - openWindow(value: WindowDestinationEditor.mentionStatusEditor(account: viewModel.status.reblog?.account ?? viewModel.status.account, visibility: .direct)) + openWindow( + value: WindowDestinationEditor.mentionStatusEditor( + account: viewModel.status.reblog?.account ?? viewModel.status.account, + visibility: .direct)) #else - viewModel.routerPath.presentedSheet = .mentionStatusEditor(account: viewModel.status.reblog?.account ?? viewModel.status.account, visibility: .direct) + viewModel.routerPath.presentedSheet = .mentionStatusEditor( + account: viewModel.status.reblog?.account ?? viewModel.status.account, visibility: .direct + ) #endif } label: { Label("status.action.message", systemImage: "tray.full") @@ -245,7 +290,8 @@ struct StatusRowContextMenu: View { Task { do { let operationAccount = viewModel.status.reblog?.account ?? viewModel.status.account - viewModel.authorRelationship = try await client.post(endpoint: Accounts.unblock(id: operationAccount.id)) + viewModel.authorRelationship = try await client.post( + endpoint: Accounts.unblock(id: operationAccount.id)) } catch {} } } label: { diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowDetailView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowDetailView.swift index b3681705..9e7d4085 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowDetailView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowDetailView.swift @@ -16,15 +16,15 @@ struct StatusRowDetailView: View { Divider() HStack { Group { - Text(viewModel.status.createdAt.asDate, style: .date) + - Text("status.summary.at-time") + - Text(viewModel.status.createdAt.asDate, style: .time) + - Text(" ·") + Text(viewModel.status.createdAt.asDate, style: .date) + Text("status.summary.at-time") + + Text(viewModel.status.createdAt.asDate, style: .time) + Text(" ·") Image(systemName: viewModel.status.visibility.iconName) .accessibilityHidden(true) }.accessibilityElement(children: .combine) Spacer() - if let name = viewModel.status.application?.name, let url = viewModel.status.application?.website { + if let name = viewModel.status.application?.name, + let url = viewModel.status.application?.website + { Button { openURL(url) } label: { @@ -44,10 +44,8 @@ struct StatusRowDetailView: View { if let editedAt = viewModel.status.editedAt { Divider() HStack { - Text("status.summary.edited-time") + - Text(editedAt.asDate, style: .date) + - Text("status.summary.at-time") + - Text(editedAt.asDate, style: .time) + Text("status.summary.edited-time") + Text(editedAt.asDate, style: .date) + + Text("status.summary.at-time") + Text(editedAt.asDate, style: .time) } .frame(maxWidth: .infinity, alignment: .leading) .onTapGesture { diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowHeaderView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowHeaderView.swift index 3de75630..3c357b15 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowHeaderView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowHeaderView.swift @@ -26,7 +26,11 @@ struct StatusRowHeaderView: View { } } .accessibilityElement(children: .combine) - .accessibilityLabel(Text("\(viewModel.finalStatus.account.safeDisplayName), \(viewModel.finalStatus.createdAt.relativeFormatted)")) + .accessibilityLabel( + Text( + "\(viewModel.finalStatus.account.safeDisplayName), \(viewModel.finalStatus.createdAt.relativeFormatted)" + ) + ) .accessibilityAction { viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account) } @@ -37,22 +41,24 @@ struct StatusRowHeaderView: View { HStack(alignment: .center) { if theme.avatarPosition == .top { AvatarView(viewModel.finalStatus.account.avatar) - #if targetEnvironment(macCatalyst) - .accountPopover(viewModel.finalStatus.account) - #endif + #if targetEnvironment(macCatalyst) + .accountPopover(viewModel.finalStatus.account) + #endif } VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline, spacing: 2) { Group { - EmojiTextApp(viewModel.finalStatus.account.cachedDisplayName, - emojis: viewModel.finalStatus.account.emojis) - .fixedSize(horizontal: false, vertical: true) - .font(.scaledSubheadline) - .foregroundColor(theme.labelColor) - .emojiText.size(Font.scaledSubheadlineFont.emojiSize) - .emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset) - .fontWeight(.semibold) - .lineLimit(1) + EmojiTextApp( + viewModel.finalStatus.account.cachedDisplayName, + emojis: viewModel.finalStatus.account.emojis + ) + .fixedSize(horizontal: false, vertical: true) + .font(.scaledSubheadline) + .foregroundColor(theme.labelColor) + .emojiText.size(Font.scaledSubheadlineFont.emojiSize) + .emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset) + .fontWeight(.semibold) + .lineLimit(1) #if targetEnvironment(macCatalyst) .accountPopover(viewModel.finalStatus.account) #endif @@ -66,14 +72,16 @@ struct StatusRowHeaderView: View { .layoutPriority(1) } if !redactionReasons.contains(.placeholder) { - if (theme.displayFullUsername && theme.avatarPosition == .leading) || - theme.avatarPosition == .top + if (theme.displayFullUsername && theme.avatarPosition == .leading) + || theme.avatarPosition == .top { - Text("@\(theme.displayFullUsername ? viewModel.finalStatus.account.acct : viewModel.finalStatus.account.username)") - .fixedSize(horizontal: false, vertical: true) - .font(.scaledFootnote) - .foregroundStyle(.secondary) - .lineLimit(1) + Text( + "@\(theme.displayFullUsername ? viewModel.finalStatus.account.acct : viewModel.finalStatus.account.username)" + ) + .fixedSize(horizontal: false, vertical: true) + .font(.scaledFootnote) + .foregroundStyle(.secondary) + .lineLimit(1) #if targetEnvironment(macCatalyst) .accountPopover(viewModel.finalStatus.account) #endif @@ -93,10 +101,12 @@ struct StatusRowHeaderView: View { } private var dateView: some View { - Text("\(Image(systemName: viewModel.finalStatus.visibility.iconName)) ⸱ \(viewModel.finalStatus.createdAt.relativeFormatted)") - .fixedSize(horizontal: false, vertical: true) - .font(.scaledFootnote) - .foregroundStyle(.secondary) - .lineLimit(1) + Text( + "\(Image(systemName: viewModel.finalStatus.visibility.iconName)) ⸱ \(viewModel.finalStatus.createdAt.relativeFormatted)" + ) + .fixedSize(horizontal: false, vertical: true) + .font(.scaledFootnote) + .foregroundStyle(.secondary) + .lineLimit(1) } } diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowMediaPreviewView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowMediaPreviewView.swift index cbdb30db..8ae2d9ca 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowMediaPreviewView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowMediaPreviewView.swift @@ -86,7 +86,7 @@ public struct StatusRowMediaPreviewView: View { } } #if os(visionOS) - .hoverEffect() + .hoverEffect() #endif } } @@ -132,8 +132,10 @@ private struct MediaPreview: View { image .resizable() .aspectRatio(contentMode: .fill) - .frame(width: displayData.isLandscape ? imageMaxHeight * 1.2 : imageMaxHeight / 1.5, - height: imageMaxHeight) + .frame( + width: displayData.isLandscape ? imageMaxHeight * 1.2 : imageMaxHeight / 1.5, + height: imageMaxHeight + ) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(.gray.opacity(0.35), lineWidth: 1) @@ -154,8 +156,10 @@ private struct MediaPreview: View { .accessibilityAddTraits(.startsMediaSession) } } - .frame(width: displayData.isLandscape ? imageMaxHeight * 1.2 : imageMaxHeight / 1.5, - height: imageMaxHeight) + .frame( + width: displayData.isLandscape ? imageMaxHeight * 1.2 : imageMaxHeight / 1.5, + height: imageMaxHeight + ) .clipped() .cornerRadius(10) // #965: do not create overlapping tappable areas, when multiple images are shown @@ -262,10 +266,10 @@ struct AltTextButton: View { var body: some View { if !isInCaptureMode, - let text, - !text.isEmpty, - !isCompact, - preferences.showAltTextForMedia + let text, + !text.isEmpty, + !isCompact, + preferences.showAltTextForMedia { Button { isDisplayingAlert = true @@ -283,7 +287,7 @@ struct AltTextButton: View { .addTranslateView(isPresented: $isDisplayingTranslation, text: text) #endif #if os(visionOS) - .clipShape(Capsule()) + .clipShape(Capsule()) #endif .cornerRadius(4) .padding(theme.statusDisplayStyle == .compact ? 0 : 10) @@ -371,7 +375,7 @@ struct WrapperForPreview: View { VStack { ScrollView { VStack { - ForEach(1 ..< 5) { number in + ForEach(1..<5) { number in VStack { Text("Preview for \(number) item(s)") StatusRowMediaPreviewView( @@ -398,7 +402,8 @@ struct WrapperForPreview: View { .padding() } - private static let url = URL(string: "https://www.upwork.com/catalog-images/c5dffd9b5094556adb26e0a193a1c494")! + private static let url = URL( + string: "https://www.upwork.com/catalog-images/c5dffd9b5094556adb26e0a193a1c494")! private static let attachment = MediaAttachment.imageWith(url: url) private static let local = Locale(identifier: "en") } @@ -448,9 +453,9 @@ private struct FeaturedImagePreView: View { RoundedRectangle(cornerRadius: 10) .stroke(.gray.opacity(0.35), lineWidth: 1) ) - #if os(visionOS) - .hoverEffect() - #endif + #if os(visionOS) + .hoverEffect() + #endif } } .overlay { @@ -486,7 +491,9 @@ private struct FeaturedImagePreView: View { return calculateSize(proposal) } - func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) { + func placeSubviews( + in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout () + ) { guard let view = subviews.first else { return } let size = if let maxSize { maxSize } else { calculateSize(proposal) } @@ -499,7 +506,8 @@ private struct FeaturedImagePreView: View { case (0, _), (_, 0): size = CGSize.zero - case (nil, nil), (nil, .some(.infinity)), (.some(.infinity), .some(.infinity)), (.some(.infinity), nil): + case (nil, nil), (nil, .some(.infinity)), (.some(.infinity), .some(.infinity)), + (.some(.infinity), nil): size = CGSize(width: originalWidth, height: originalWidth) case let (nil, .some(height)), let (.some(.infinity), .some(height)): diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowPremiumView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowPremiumView.swift index 248336bd..24db7389 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowPremiumView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowPremiumView.swift @@ -4,7 +4,7 @@ import SwiftUI struct StatusRowPremiumView: View { @Environment(\.isHomeTimeline) private var isHomeTimeline - + let viewModel: StatusRowViewModel var body: some View { diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowReblogView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowReblogView.swift index 3ff93aa2..f4476242 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowReblogView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowReblogView.swift @@ -9,7 +9,9 @@ struct StatusRowReblogView: View { HStack(spacing: 2) { Image("Rocket.Fill") AvatarView(viewModel.status.account.avatar, config: .boost) - EmojiTextApp(.init(stringValue: viewModel.status.account.safeDisplayName), emojis: viewModel.status.account.emojis) + EmojiTextApp( + .init(stringValue: viewModel.status.account.safeDisplayName), + emojis: viewModel.status.account.emojis) Text("status.row.was-boosted") } .accessibilityElement(children: .combine) diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowSpoilerView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowSpoilerView.swift index 502e0458..2bcf2990 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowSpoilerView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowSpoilerView.swift @@ -31,7 +31,7 @@ struct StatusRowSpoilerView: View { .accessibilityHidden(true) } .contentShape(Rectangle()) - .onTapGesture { // make whole row tapable to make up for smaller button size + .onTapGesture { // make whole row tapable to make up for smaller button size withAnimation { displaySpoiler.toggle() } diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowSwipeView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowSwipeView.swift index 8f1561bd..e6952f37 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowSwipeView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowSwipeView.swift @@ -15,7 +15,8 @@ struct StatusRowSwipeView: View { } func privateBoost() -> Bool { - viewModel.status.visibility == .priv && viewModel.status.account.id == currentAccount.account?.id + viewModel.status.visibility == .priv + && viewModel.status.account.id == currentAccount.account?.id } var viewModel: StatusRowViewModel @@ -34,11 +35,17 @@ struct StatusRowSwipeView: View { private var trailingSwipeActions: some View { if preferences.swipeActionsStatusTrailingRight != StatusAction.none, !viewModel.isRemote { makeSwipeButton(action: preferences.swipeActionsStatusTrailingRight) - .tint(preferences.swipeActionsStatusTrailingRight.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: true)) + .tint( + preferences.swipeActionsStatusTrailingRight.color( + themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, + outside: true)) } if preferences.swipeActionsStatusTrailingLeft != StatusAction.none, !viewModel.isRemote { makeSwipeButton(action: preferences.swipeActionsStatusTrailingLeft) - .tint(preferences.swipeActionsStatusTrailingLeft.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: false)) + .tint( + preferences.swipeActionsStatusTrailingLeft.color( + themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, + outside: false)) } } @@ -46,11 +53,17 @@ struct StatusRowSwipeView: View { private var leadingSwipeActions: some View { if preferences.swipeActionsStatusLeadingLeft != StatusAction.none, !viewModel.isRemote { makeSwipeButton(action: preferences.swipeActionsStatusLeadingLeft) - .tint(preferences.swipeActionsStatusLeadingLeft.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: true)) + .tint( + preferences.swipeActionsStatusLeadingLeft.color( + themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, + outside: true)) } if preferences.swipeActionsStatusLeadingRight != StatusAction.none, !viewModel.isRemote { makeSwipeButton(action: preferences.swipeActionsStatusLeadingRight) - .tint(preferences.swipeActionsStatusLeadingRight.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: false)) + .tint( + preferences.swipeActionsStatusLeadingRight.color( + themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, + outside: false)) } } @@ -58,10 +71,13 @@ struct StatusRowSwipeView: View { private func makeSwipeButton(action: StatusAction) -> some View { switch action { case .reply: - makeSwipeButtonForRouterPath(action: action, destination: .replyToStatusEditor(status: viewModel.status)) + makeSwipeButtonForRouterPath( + action: action, destination: .replyToStatusEditor(status: viewModel.status)) case .quote: - makeSwipeButtonForRouterPath(action: action, destination: .quoteStatusEditor(status: viewModel.status)) - .disabled(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv) + makeSwipeButtonForRouterPath( + action: action, destination: .quoteStatusEditor(status: viewModel.status) + ) + .disabled(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv) case .favorite: makeSwipeButtonForTask(action: action) { await statusDataController.toggleFavorite(remoteStatus: nil) @@ -70,7 +86,11 @@ struct StatusRowSwipeView: View { makeSwipeButtonForTask(action: action, privateBoost: privateBoost()) { await statusDataController.toggleReblog(remoteStatus: nil) } - .disabled(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv && viewModel.status.account.id != currentAccount.account?.id) + .disabled( + viewModel.status.visibility == .direct + || viewModel.status.visibility == .priv + && viewModel.status.account.id != currentAccount.account?.id + ) case .bookmark: makeSwipeButtonForTask(action: action) { await statusDataController.toggleBookmark(remoteStatus: nil) @@ -81,7 +101,9 @@ struct StatusRowSwipeView: View { } @ViewBuilder - private func makeSwipeButtonForRouterPath(action: StatusAction, destination: SheetDestination) -> some View { + private func makeSwipeButtonForRouterPath(action: StatusAction, destination: SheetDestination) + -> some View + { Button { HapticManager.shared.fireHaptic(.notification(.success)) viewModel.routerPath.presentedSheet = destination @@ -91,42 +113,55 @@ struct StatusRowSwipeView: View { } @ViewBuilder - private func makeSwipeButtonForTask(action: StatusAction, privateBoost: Bool = false, task: @escaping () async -> Void) -> some View { + private func makeSwipeButtonForTask( + action: StatusAction, privateBoost: Bool = false, task: @escaping () async -> Void + ) -> some View { Button { Task { HapticManager.shared.fireHaptic(.notification(.success)) await task() } } label: { - makeSwipeLabel(action: action, style: preferences.swipeActionsIconStyle, privateBoost: privateBoost) + makeSwipeLabel( + action: action, style: preferences.swipeActionsIconStyle, privateBoost: privateBoost) } } @ViewBuilder - private func makeSwipeLabel(action: StatusAction, style: UserPreferences.SwipeActionsIconStyle, privateBoost: Bool = false) -> some View { + private func makeSwipeLabel( + action: StatusAction, style: UserPreferences.SwipeActionsIconStyle, privateBoost: Bool = false + ) -> some View { switch style { case .iconOnly: - Label(action.displayName(isReblogged: statusDataController.isReblogged, - isFavorited: statusDataController.isFavorited, - isBookmarked: statusDataController.isBookmarked, - privateBoost: privateBoost), - imageNamed: action.iconName(isReblogged: statusDataController.isReblogged, - isFavorited: statusDataController.isFavorited, - isBookmarked: statusDataController.isBookmarked, - privateBoost: privateBoost)) - .labelStyle(.iconOnly) - .environment(\.symbolVariants, .none) + Label( + action.displayName( + isReblogged: statusDataController.isReblogged, + isFavorited: statusDataController.isFavorited, + isBookmarked: statusDataController.isBookmarked, + privateBoost: privateBoost), + imageNamed: action.iconName( + isReblogged: statusDataController.isReblogged, + isFavorited: statusDataController.isFavorited, + isBookmarked: statusDataController.isBookmarked, + privateBoost: privateBoost) + ) + .labelStyle(.iconOnly) + .environment(\.symbolVariants, .none) case .iconWithText: - Label(action.displayName(isReblogged: statusDataController.isReblogged, - isFavorited: statusDataController.isFavorited, - isBookmarked: statusDataController.isBookmarked, - privateBoost: privateBoost), - imageNamed: action.iconName(isReblogged: statusDataController.isReblogged, - isFavorited: statusDataController.isFavorited, - isBookmarked: statusDataController.isBookmarked, - privateBoost: privateBoost)) - .labelStyle(.titleAndIcon) - .environment(\.symbolVariants, .none) + Label( + action.displayName( + isReblogged: statusDataController.isReblogged, + isFavorited: statusDataController.isFavorited, + isBookmarked: statusDataController.isBookmarked, + privateBoost: privateBoost), + imageNamed: action.iconName( + isReblogged: statusDataController.isReblogged, + isFavorited: statusDataController.isFavorited, + isBookmarked: statusDataController.isBookmarked, + privateBoost: privateBoost) + ) + .labelStyle(.titleAndIcon) + .environment(\.symbolVariants, .none) } } } diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTextView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTextView.swift index 5ffbeeba..b7bc1121 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTextView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTextView.swift @@ -15,17 +15,27 @@ struct StatusRowTextView: View { var body: some View { VStack { HStack { - EmojiTextApp(statusDataController.content, - emojis: viewModel.finalStatus.emojis, - language: viewModel.finalStatus.language, - lineLimit: viewModel.lineLimit) - .fixedSize(horizontal: false, vertical: true) - .font(isFocused ? .scaledBodyFocused : .scaledBody) - .lineSpacing(CGFloat(theme.lineSpacing)) - .foregroundColor(viewModel.textDisabled ? .gray : theme.labelColor) - .emojiText.size(isFocused ? Font.scaledBodyFocusedFont.emojiSize : Font.scaledBodyFont.emojiSize) - .emojiText.baselineOffset(isFocused ? Font.scaledBodyFocusedFont.emojiBaselineOffset : Font.scaledBodyFont.emojiBaselineOffset) - .environment(\.openURL, OpenURLAction { url in + EmojiTextApp( + statusDataController.content, + emojis: viewModel.finalStatus.emojis, + language: viewModel.finalStatus.language, + lineLimit: viewModel.lineLimit + ) + .fixedSize(horizontal: false, vertical: true) + .font(isFocused ? .scaledBodyFocused : .scaledBody) + .lineSpacing(CGFloat(theme.lineSpacing)) + .foregroundColor(viewModel.textDisabled ? .gray : theme.labelColor) + .emojiText.size( + isFocused ? Font.scaledBodyFocusedFont.emojiSize : Font.scaledBodyFont.emojiSize + ) + .emojiText.baselineOffset( + isFocused + ? Font.scaledBodyFocusedFont.emojiBaselineOffset + : Font.scaledBodyFont.emojiBaselineOffset + ) + .environment( + \.openURL, + OpenURLAction { url in viewModel.routerPath.handleStatus(status: viewModel.finalStatus, url: url) }) } @@ -36,7 +46,7 @@ struct StatusRowTextView: View { @ViewBuilder func makeCollapseButton() -> some View { - if let _ = viewModel.lineLimit { + if viewModel.lineLimit != nil { HStack(alignment: .top) { Text("status.show-full-post") .font(.system(.subheadline, weight: .bold)) @@ -54,7 +64,7 @@ struct StatusRowTextView: View { .accessibilityHidden(true) } .contentShape(Rectangle()) - .onTapGesture { // make whole row tapable to make up for smaller button size + .onTapGesture { // make whole row tapable to make up for smaller button size withAnimation { viewModel.isCollapsed.toggle() } diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift index 04da7a45..fc133fdf 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift @@ -16,9 +16,9 @@ struct StatusRowTranslateView: View { let statusLang = viewModel.getStatusLang() if let userLang = preferences.serverPreferences?.postLanguage, - preferences.showTranslateButton, - !viewModel.finalStatus.content.asRawText.isEmpty, - viewModel.translation == nil + preferences.showTranslateButton, + !viewModel.finalStatus.content.asRawText.isEmpty, + viewModel.translation == nil { return userLang != statusLang } else { @@ -38,9 +38,9 @@ struct StatusRowTranslateView: View { @ViewBuilder var translateButton: some View { if !isInCaptureMode, - !isCompact, - let userLang = preferences.serverPreferences?.postLanguage, - shouldShowTranslateButton + !isCompact, + let userLang = preferences.serverPreferences?.postLanguage, + shouldShowTranslateButton { Button { Task { @@ -70,14 +70,19 @@ struct StatusRowTranslateView: View { } } - if let translation = viewModel.translation, !viewModel.isLoadingTranslation, preferences.preferredTranslationType != .useApple { + if let translation = viewModel.translation, !viewModel.isLoadingTranslation, + preferences.preferredTranslationType != .useApple + { GroupBox { VStack(alignment: .leading, spacing: 4) { Text(translation.content.asSafeMarkdownAttributedString) .font(.scaledBody) - Text(getLocalizedString(langCode: translation.detectedSourceLanguage, provider: translation.provider)) - .font(.footnote) - .foregroundStyle(.secondary) + Text( + getLocalizedString( + langCode: translation.detectedSourceLanguage, provider: translation.provider) + ) + .font(.footnote) + .foregroundStyle(.secondary) } } .fixedSize(horizontal: false, vertical: true) diff --git a/Packages/StatusKit/Sources/StatusKit/Share/StatusRowSelectableTextView.swift b/Packages/StatusKit/Sources/StatusKit/Share/StatusRowSelectableTextView.swift index 829a82a8..b0f32206 100644 --- a/Packages/StatusKit/Sources/StatusKit/Share/StatusRowSelectableTextView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Share/StatusRowSelectableTextView.swift @@ -1,34 +1,34 @@ -import SwiftUI import DesignSystem +import SwiftUI struct StatusRowSelectableTextView: View { @Environment(\.dismiss) private var dismiss @Environment(Theme.self) private var theme - + let content: AttributedString var body: some View { NavigationStack { SelectableText(content: content) .padding() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - dismiss() - } label: { - Text("action.done").bold() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + dismiss() + } label: { + Text("action.done").bold() + } } } - } - .navigationTitle("status.action.select-text") - .navigationBarTitleDisplayMode(.inline) + .navigationTitle("status.action.select-text") + .navigationBarTitleDisplayMode(.inline) } .presentationBackground(.ultraThinMaterial) .presentationCornerRadius(16) } } -fileprivate struct SelectableText: UIViewRepresentable { +private struct SelectableText: UIViewRepresentable { let content: AttributedString func makeUIView(context _: Context) -> UITextView { diff --git a/Packages/StatusKit/Sources/StatusKit/Share/StatusRowShareAsImageView.swift b/Packages/StatusKit/Sources/StatusKit/Share/StatusRowShareAsImageView.swift index 522943cf..a5f61440 100644 --- a/Packages/StatusKit/Sources/StatusKit/Share/StatusRowShareAsImageView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Share/StatusRowShareAsImageView.swift @@ -1,34 +1,35 @@ -import SwiftUI +import DesignSystem import Env import Network -import DesignSystem +import SwiftUI struct StatusRowShareAsImageView: View { @Environment(\.dismiss) private var dismiss @Environment(Theme.self) private var theme - + let viewModel: StatusRowViewModel @StateObject var renderer: ImageRenderer - + var rendererImage: Image { Image(uiImage: renderer.uiImage ?? UIImage()) } - + var body: some View { NavigationStack { Form { Section { Button { - viewModel.routerPath.presentedSheet = .shareImage(image: renderer.uiImage ?? UIImage(), - status: viewModel.status) + viewModel.routerPath.presentedSheet = .shareImage( + image: renderer.uiImage ?? UIImage(), + status: viewModel.status) } label: { Label("status.action.share-image", systemImage: "square.and.arrow.up") } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor.opacity(0.4)) + .listRowBackground(theme.primaryBackgroundColor.opacity(0.4)) #endif - + Section { rendererImage .resizable() diff --git a/Packages/Timeline/Package.swift b/Packages/Timeline/Package.swift index 258f5203..7c656fcf 100644 --- a/Packages/Timeline/Package.swift +++ b/Packages/Timeline/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library( name: "Timeline", targets: ["Timeline"] - ), + ) ], dependencies: [ .package(name: "Network", path: "../Network"), @@ -38,7 +38,7 @@ let package = Package( .product(name: "Bodega", package: "Bodega"), ], swiftSettings: [ - .swiftLanguageMode(.v6), + .swiftLanguageMode(.v6) ] ), .testTarget( diff --git a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift index 815c8c9c..10bc7ada 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift @@ -122,7 +122,7 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable { case let .hashtag(tag, _): "#\(tag)" case let .tagGroup(title, _, _): - LocalizedStringKey(title) // ?? not sure since this can't be localized. + LocalizedStringKey(title) // ?? not sure since this can't be localized. case let .list(list): LocalizedStringKey(list.title) case let .remoteLocal(server, _): @@ -157,20 +157,26 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable { } } - public func endpoint(sinceId: String?, - maxId: String?, - minId: String?, - offset: Int?, - limit: Int?) -> Endpoint { + public func endpoint( + sinceId: String?, + maxId: String?, + minId: String?, + offset: Int?, + limit: Int? + ) -> Endpoint { switch self { - case .federated: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false, limit: limit) - case .local: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true, limit: limit) + case .federated: + return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false, limit: limit) + case .local: + return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true, limit: limit) case let .remoteLocal(_, filter): switch filter { case .local: - return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true, limit: limit) + return Timelines.pub( + sinceId: sinceId, maxId: maxId, minId: minId, local: true, limit: limit) case .federated: - return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false, limit: limit) + return Timelines.pub( + sinceId: sinceId, maxId: maxId, minId: minId, local: false, limit: limit) case .trending: return Trends.statuses(offset: offset) } @@ -178,17 +184,20 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable { case .resume: return Timelines.home(sinceId: nil, maxId: nil, minId: nil, limit: limit) case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId, limit: limit) case .trending: return Trends.statuses(offset: offset) - case let .link(url, _): return Timelines.link(url: url, sinceId: sinceId, maxId: maxId, minId: minId) - case let .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId) + case let .link(url, _): + return Timelines.link(url: url, 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): if let accountId { - return Accounts.statuses(id: accountId, - sinceId: nil, - tag: tag, - onlyMedia: false, - excludeReplies: false, - excludeReblogs: false, - pinned: nil) + return Accounts.statuses( + id: accountId, + sinceId: nil, + tag: tag, + onlyMedia: false, + excludeReplies: false, + excludeReblogs: false, + pinned: nil) } else { return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId, minId: minId) } @@ -321,7 +330,7 @@ extension TimelineFilter: Codable { extension TimelineFilter: RawRepresentable { public init?(rawValue: String) { guard let data = rawValue.data(using: .utf8), - let result = try? JSONDecoder().decode(TimelineFilter.self, from: data) + let result = try? JSONDecoder().decode(TimelineFilter.self, from: data) else { return nil } @@ -330,7 +339,7 @@ extension TimelineFilter: RawRepresentable { public var rawValue: String { guard let data = try? JSONEncoder().encode(self), - let result = String(data: data, encoding: .utf8) + let result = String(data: data, encoding: .utf8) else { return "[]" } @@ -381,7 +390,7 @@ extension RemoteTimelineFilter: Codable { extension RemoteTimelineFilter: RawRepresentable { public init?(rawValue: String) { guard let data = rawValue.data(using: .utf8), - let result = try? JSONDecoder().decode(RemoteTimelineFilter.self, from: data) + let result = try? JSONDecoder().decode(RemoteTimelineFilter.self, from: data) else { return nil } @@ -390,7 +399,7 @@ extension RemoteTimelineFilter: RawRepresentable { public var rawValue: String { guard let data = try? JSONEncoder().encode(self), - let result = String(data: data, encoding: .utf8) + let result = String(data: data, encoding: .utf8) else { return "[]" } diff --git a/Packages/Timeline/Sources/Timeline/TimelineUnreadStatusesObserver.swift b/Packages/Timeline/Sources/Timeline/TimelineUnreadStatusesObserver.swift index 6fdb5beb..2d7f6d04 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineUnreadStatusesObserver.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineUnreadStatusesObserver.swift @@ -23,7 +23,7 @@ import SwiftUI func removeStatus(status: Status) { if !disableUpdate, let index = pendingStatuses.firstIndex(of: status.id) { - pendingStatuses.removeSubrange(index ... (pendingStatuses.count - 1)) + pendingStatuses.removeSubrange(index...(pendingStatuses.count - 1)) HapticManager.shared.fireHaptic(.timeline) } } @@ -57,7 +57,9 @@ struct TimelineUnreadStatusesView: View { } } } - .accessibilityLabel("accessibility.tabs.timeline.unread-posts.label-\(observer.pendingStatusesCount)") + .accessibilityLabel( + "accessibility.tabs.timeline.unread-posts.label-\(observer.pendingStatusesCount)" + ) .accessibilityHint("accessibility.tabs.timeline.unread-posts.hint") #if os(visionOS) .buttonStyle(.bordered) @@ -66,7 +68,7 @@ struct TimelineUnreadStatusesView: View { .buttonStyle(.bordered) .background(Material.ultraThick) #endif - .cornerRadius(8) + .cornerRadius(8) #if !os(visionOS) .foregroundStyle(.secondary) .overlay( @@ -74,8 +76,8 @@ struct TimelineUnreadStatusesView: View { .stroke(theme.tintColor, lineWidth: 1) ) #endif - .padding(8) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: preferences.pendingLocation) + .padding(8) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: preferences.pendingLocation) } } } diff --git a/Packages/Timeline/Sources/Timeline/View/TimelineContentFilterView.swift b/Packages/Timeline/Sources/Timeline/View/TimelineContentFilterView.swift index 39caf1e4..600a8dd4 100644 --- a/Packages/Timeline/Sources/Timeline/View/TimelineContentFilterView.swift +++ b/Packages/Timeline/Sources/Timeline/View/TimelineContentFilterView.swift @@ -30,7 +30,7 @@ public struct TimelineContentFilterView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor.opacity(0.3)) + .listRowBackground(theme.primaryBackgroundColor.opacity(0.3)) #endif Section { @@ -46,7 +46,7 @@ public struct TimelineContentFilterView: View { } } #if !os(visionOS) - .listRowBackground(theme.primaryBackgroundColor.opacity(0.3)) + .listRowBackground(theme.primaryBackgroundColor.opacity(0.3)) #endif } .navigationTitle("timeline.content-filter.title") diff --git a/Packages/Timeline/Sources/Timeline/View/TimelineHeaderView.swift b/Packages/Timeline/Sources/Timeline/View/TimelineHeaderView.swift index 93793868..2194de70 100644 --- a/Packages/Timeline/Sources/Timeline/View/TimelineHeaderView.swift +++ b/Packages/Timeline/Sources/Timeline/View/TimelineHeaderView.swift @@ -13,16 +13,20 @@ struct TimelineHeaderView: View { Spacer() } #if os(visionOS) - .listRowBackground(RoundedRectangle(cornerRadius: 8) - .foregroundStyle(.background).hoverEffect()) - .listRowHoverEffectDisabled() + .listRowBackground( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.background).hoverEffect() + ) + .listRowHoverEffectDisabled() #else - .listRowBackground(theme.secondaryBackgroundColor) + .listRowBackground(theme.secondaryBackgroundColor) #endif .listRowSeparator(.hidden) - .listRowInsets(.init(top: 8, - leading: .layoutPadding, - bottom: 8, - trailing: .layoutPadding)) + .listRowInsets( + .init( + top: 8, + leading: .layoutPadding, + bottom: 8, + trailing: .layoutPadding)) } } diff --git a/Packages/Timeline/Sources/Timeline/View/TimelineQuickAccessPills.swift b/Packages/Timeline/Sources/Timeline/View/TimelineQuickAccessPills.swift index e094507b..58d9ba93 100644 --- a/Packages/Timeline/Sources/Timeline/View/TimelineQuickAccessPills.swift +++ b/Packages/Timeline/Sources/Timeline/View/TimelineQuickAccessPills.swift @@ -33,7 +33,7 @@ struct TimelineQuickAccessPills: View { switch filter { case let .list(list): if let accountList = lists.first(where: { $0.id == list.id }), - accountList.title != list.title + accountList.title != list.title { filters[index] = .list(list: accountList) } @@ -64,8 +64,9 @@ struct TimelineQuickAccessPills: View { } label: { switch filter { case .hashtag: - Label(filter.title.replacingOccurrences(of: "#", with: ""), - systemImage: filter.iconName()) + Label( + filter.title.replacingOccurrences(of: "#", with: ""), + systemImage: filter.iconName()) case let .list(list): if let list = currentAccount.lists.first(where: { $0.id == list.id }) { Label(list.title, systemImage: filter.iconName()) @@ -79,12 +80,16 @@ struct TimelineQuickAccessPills: View { draggedFilter = filter return NSItemProvider() } - .onDrop(of: [.text], delegate: PillDropDelegate(destinationItem: filter, - items: $pinnedFilters, - draggedItem: $draggedFilter)) + .onDrop( + of: [.text], + delegate: PillDropDelegate( + destinationItem: filter, + items: $pinnedFilters, + draggedItem: $draggedFilter) + ) .buttonBorderShape(.capsule) .controlSize(.mini) - + } private func isFilterSupported(_ filter: TimelineFilter) -> Bool { @@ -118,7 +123,9 @@ struct PillDropDelegate: DropDelegate { let toIndex = items.firstIndex(of: destinationItem) if let toIndex, fromIndex != toIndex { withAnimation { - self.items.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? (toIndex + 1) : toIndex) + self.items.move( + fromOffsets: IndexSet(integer: fromIndex), + toOffset: toIndex > fromIndex ? (toIndex + 1) : toIndex) } } } diff --git a/Packages/Timeline/Sources/Timeline/View/TimelineTagGroupheaderView.swift b/Packages/Timeline/Sources/Timeline/View/TimelineTagGroupheaderView.swift index 941daaf3..3ea3c22a 100644 --- a/Packages/Timeline/Sources/Timeline/View/TimelineTagGroupheaderView.swift +++ b/Packages/Timeline/Sources/Timeline/View/TimelineTagGroupheaderView.swift @@ -27,9 +27,12 @@ struct TimelineTagGroupheaderView: View { } .scrollIndicators(.hidden) Button("status.action.edit") { - routerPath.presentedSheet = .editTagGroup(tagGroup: group, onSaved: { group in - timeline = .tagGroup(title: group.title, tags: group.tags, symbolName: group.symbolName) - }) + routerPath.presentedSheet = .editTagGroup( + tagGroup: group, + onSaved: { group in + timeline = .tagGroup( + title: group.title, tags: group.tags, symbolName: group.symbolName) + }) } .buttonStyle(.bordered) } diff --git a/Packages/Timeline/Sources/Timeline/View/TimelineView.swift b/Packages/Timeline/Sources/Timeline/View/TimelineView.swift index 4fc1e786..8bf8645e 100644 --- a/Packages/Timeline/Sources/Timeline/View/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/View/TimelineView.swift @@ -11,7 +11,7 @@ import SwiftUI public struct TimelineView: View { @Environment(\.scenePhase) private var scenePhase @Environment(\.selectedTabScrollToTop) private var selectedTabScrollToTop - + @Environment(Theme.self) private var theme @Environment(CurrentAccount.self) private var account @Environment(StreamWatcher.self) private var watcher @@ -20,7 +20,7 @@ public struct TimelineView: View { @State private var viewModel = TimelineViewModel() @State private var contentFilter = TimelineContentFilter.shared - + @State private var scrollToIdAnimated: String? = nil @State private var wasBackgrounded: Bool = false @@ -33,11 +33,12 @@ public struct TimelineView: View { private let canFilterTimeline: Bool - public init(timeline: Binding, - pinnedFilters: Binding<[TimelineFilter]>, - selectedTagGroup: Binding, - canFilterTimeline: Bool) - { + public init( + timeline: Binding, + pinnedFilters: Binding<[TimelineFilter]>, + selectedTagGroup: Binding, + canFilterTimeline: Bool + ) { _timeline = timeline _pinnedFilters = pinnedFilters _selectedTagGroup = selectedTagGroup @@ -102,8 +103,8 @@ public struct TimelineView: View { switch timeline { case let .list(list): if let accountList = lists.first(where: { $0.id == list.id }), - list.id == accountList.id, - accountList.title != list.title + list.id == accountList.id, + accountList.title != list.title { timeline = .list(list: accountList) } @@ -160,7 +161,7 @@ public struct TimelineView: View { } } } - + private var listView: some View { ScrollViewReader { proxy in List { @@ -169,7 +170,8 @@ public struct TimelineView: View { TimelineTagHeaderView(tag: $viewModel.tag) switch viewModel.timeline { case .remoteLocal: - StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath, isRemote: true) + StatusesListView( + fetcher: viewModel, client: client, routerPath: routerPath, isRemote: true) default: StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath) .environment(\.isHomeTimeline, timeline == .home) @@ -178,10 +180,10 @@ public struct TimelineView: View { .id(client.id) .environment(\.defaultMinListRowHeight, 1) .listStyle(.plain) -#if !os(visionOS) - .scrollContentBackground(.hidden) - .background(theme.primaryBackgroundColor) -#endif + #if !os(visionOS) + .scrollContentBackground(.hidden) + .background(theme.primaryBackgroundColor) + #endif .onChange(of: viewModel.scrollToId) { _, newValue in if let newValue { proxy.scrollTo(newValue, anchor: .top) @@ -205,7 +207,7 @@ public struct TimelineView: View { } } } - + @ViewBuilder private var statusesObserver: some View { if viewModel.timeline.supportNewestPagination { @@ -280,8 +282,10 @@ public struct TimelineView: View { group.tags.append(tag) } } label: { - Label(group.title, - systemImage: group.tags.contains(tag) ? "checkmark.rectangle.fill" : "checkmark.rectangle") + Label( + group.title, + systemImage: group.tags.contains(tag) + ? "checkmark.rectangle.fill" : "checkmark.rectangle") } } } diff --git a/Packages/Timeline/Sources/Timeline/View/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/View/TimelineViewModel.swift index cb630fec..7a45dbc2 100644 --- a/Packages/Timeline/Sources/Timeline/View/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/View/TimelineViewModel.swift @@ -12,8 +12,9 @@ import SwiftUI var timeline: TimelineFilter = .home { willSet { if timeline == .home, - newValue != .resume, - newValue != timeline { + newValue != .resume, + newValue != timeline + { saveMarker() } } @@ -23,9 +24,10 @@ import SwiftUI await handleLatestOrResume(oldValue) if oldValue != timeline { - Telemetry.signal("timeline.filter.updated", - parameters: ["timeline": timeline.rawValue]) - + Telemetry.signal( + "timeline.filter.updated", + parameters: ["timeline": timeline.rawValue]) + await reset() pendingStatusesObserver.pendingStatuses = [] tag = nil @@ -178,11 +180,13 @@ extension TimelineViewModel: StatusesFetcher { func fetchStatuses(from: Marker.Content) async throws { guard let client else { return } statusesState = .loading - var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, - maxId: from.lastReadId, - minId: nil, - offset: 0, - limit: 40)) + var statuses: [Status] = try await client.get( + endpoint: timeline.endpoint( + sinceId: nil, + maxId: from.lastReadId, + minId: nil, + offset: 0, + limit: 40)) StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client) @@ -228,9 +232,9 @@ extension TimelineViewModel: StatusesFetcher { // If we get statuses from the cache for the home timeline, we displays those. // Else we fetch top most page from the API. if timeline.supportNewestPagination, - let cachedStatuses = await getCachedStatuses(), - !cachedStatuses.isEmpty, - !UserPreferences.shared.fastRefreshEnabled + let cachedStatuses = await getCachedStatuses(), + !cachedStatuses.isEmpty, + !UserPreferences.shared.fastRefreshEnabled { await datasource.set(cachedStatuses) let statuses = await datasource.getFiltered() @@ -248,8 +252,9 @@ extension TimelineViewModel: StatusesFetcher { // And then we fetch statuses again toget newest statuses from there. await fetchNewestStatuses(pullToRefresh: false) } else { - var statuses: [Status] = try await statusFetcher.fetchFirstPage(client: client, - timeline: timeline) + var statuses: [Status] = try await statusFetcher.fetchFirstPage( + client: client, + timeline: timeline) StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client) @@ -258,7 +263,8 @@ extension TimelineViewModel: StatusesFetcher { statuses = await datasource.getFiltered() withAnimation { - statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage) + statusesState = .display( + statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage) } } } @@ -268,13 +274,14 @@ extension TimelineViewModel: StatusesFetcher { canStreamEvents = false let initialTimeline = timeline - let newStatuses = try await fetchAndDedupNewStatuses(latestStatus: latestStatus, - client: client) + let newStatuses = try await fetchAndDedupNewStatuses( + latestStatus: latestStatus, + client: client) guard !newStatuses.isEmpty, - isTimelineVisible, - !Task.isCancelled, - initialTimeline == timeline + isTimelineVisible, + !Task.isCancelled, + initialTimeline == timeline else { canStreamEvents = true return @@ -288,11 +295,14 @@ extension TimelineViewModel: StatusesFetcher { } } - private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async throws -> [Status] { - var newStatuses = try await statusFetcher.fetchNewPages(client: client, - timeline: timeline, - minId: latestStatus, - maxPages: 5) + private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async throws + -> [Status] + { + var newStatuses = try await statusFetcher.fetchNewPages( + client: client, + timeline: timeline, + minId: latestStatus, + maxPages: 5) let ids = await datasource.get().map(\.id) newStatuses = newStatuses.filter { status in !ids.contains(where: { $0 == status.id }) @@ -300,7 +310,7 @@ extension TimelineViewModel: StatusesFetcher { StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client) return newStatuses } - + private func updateTimelineWithNewStatuses(_ newStatuses: [Status]) async { defer { canStreamEvents = true @@ -316,8 +326,8 @@ extension TimelineViewModel: StatusesFetcher { let statuses = await datasource.getFiltered() if let topStatus = topStatus, - visibleStatuses.contains(where: { $0.id == topStatus.id }), - scrollToTopVisible + visibleStatuses.contains(where: { $0.id == topStatus.id }), + scrollToTopVisible { scrollToId = topStatus.id statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) @@ -327,7 +337,7 @@ extension TimelineViewModel: StatusesFetcher { } } } - + enum NextPageError: Error { case internalError } @@ -335,16 +345,18 @@ extension TimelineViewModel: StatusesFetcher { func fetchNextPage() async throws { let statuses = await datasource.get() guard let client, let lastId = statuses.last?.id else { throw NextPageError.internalError } - let newStatuses: [Status] = try await statusFetcher.fetchNextPage(client: client, - timeline: timeline, - lastId: lastId, - offset: statuses.count) + let newStatuses: [Status] = try await statusFetcher.fetchNextPage( + client: client, + timeline: timeline, + lastId: lastId, + offset: statuses.count) await datasource.append(contentOf: newStatuses) StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client) - statusesState = await .display(statuses: datasource.getFiltered(), - nextPageState: newStatuses.count < 20 ? .none : .hasNextPage) + statusesState = await .display( + statuses: datasource.getFiltered(), + nextPageState: newStatuses.count < 20 ? .none : .hasNextPage) } func statusDidAppear(status: Status) { @@ -381,7 +393,9 @@ extension TimelineViewModel { func saveMarker() { guard timeline == .home, let client else { return } Task { - guard let id = await cache.getLatestSeenStatus(for: client, filter: timeline.id)?.first else { return } + guard let id = await cache.getLatestSeenStatus(for: client, filter: timeline.id)?.first else { + return + } do { let _: Marker = try await client.post(endpoint: Markers.markHome(lastReadId: id)) } catch {} @@ -406,11 +420,12 @@ extension TimelineViewModel { break } } - + private func handleUpdateEvent(_ event: StreamEventUpdate, client: Client) async { guard timeline == .home, - UserPreferences.shared.isPostsStreamingEnabled, - await !datasource.contains(statusId: event.status.id) else { return } + UserPreferences.shared.isPostsStreamingEnabled, + await !datasource.contains(statusId: event.status.id) + else { return } pendingStatusesObserver.pendingStatuses.insert(event.status.id, at: 0) await datasource.insert(event.status, at: 0) @@ -420,7 +435,7 @@ extension TimelineViewModel { } private func handleDeleteEvent(_ event: StreamEventDelete) async { - if let _ = await datasource.remove(event.status) { + if await datasource.remove(event.status) != nil { await cache() await updateStatusesState() } diff --git a/Packages/Timeline/Sources/Timeline/actors/TimelineCache.swift b/Packages/Timeline/Sources/Timeline/actors/TimelineCache.swift index f2dd91d2..4ef3c451 100644 --- a/Packages/Timeline/Sources/Timeline/actors/TimelineCache.swift +++ b/Packages/Timeline/Sources/Timeline/actors/TimelineCache.swift @@ -20,7 +20,8 @@ public actor TimelineCache { public func cachedPostsCount(for client: String) async -> Int { do { let directory = FileManager.Directory.defaultStorageDirectory(appendingPath: client).url - let content = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) + let content = try FileManager.default.contentsOfDirectory( + at: directory, includingPropertiesForKeys: nil) var total: Int = await storageFor(client, "Home").allKeys().count for storage in content { if !storage.lastPathComponent.hasSuffix("sqlite3") { @@ -61,7 +62,8 @@ public actor TimelineCache { func getStatuses(for client: String, filter: String) async -> [Status]? { let engine = storageFor(client, filter) do { - return try await engine + return + try await engine .readAllData() .map { try decoder.decode(Status.self, from: $0) } .sorted(by: { $0.createdAt.asDate > $1.createdAt.asDate }) @@ -75,7 +77,8 @@ public actor TimelineCache { if filter == "Home" { UserDefaults.standard.set(statuses.map { $0.id }, forKey: "timeline-last-seen-\(client.id)") } else { - UserDefaults.standard.set(statuses.map { $0.id }, forKey: "timeline-last-seen-\(client.id)-\(filter)") + UserDefaults.standard.set( + statuses.map { $0.id }, forKey: "timeline-last-seen-\(client.id)-\(filter)") } } diff --git a/Packages/Timeline/Sources/Timeline/actors/TimelineDatasource.swift b/Packages/Timeline/Sources/Timeline/actors/TimelineDatasource.swift index a539de1d..ba6afefa 100644 --- a/Packages/Timeline/Sources/Timeline/actors/TimelineDatasource.swift +++ b/Packages/Timeline/Sources/Timeline/actors/TimelineDatasource.swift @@ -20,11 +20,12 @@ actor TimelineDatasource { let showThreads = await contentFilter.showThreads let showQuotePosts = await contentFilter.showQuotePosts return statuses.filter { status in - if status.isHidden || - !showReplies && status.inReplyToId != nil && status.inReplyToAccountId != status.account.id || - !showBoosts && status.reblog != nil || - !showThreads && status.inReplyToAccountId == status.account.id || - !showQuotePosts && !status.content.statusesURLs.isEmpty + if status.isHidden + || !showReplies && status.inReplyToId != nil + && status.inReplyToAccountId != status.account.id + || !showBoosts && status.reblog != nil + || !showThreads && status.inReplyToAccountId == status.account.id + || !showQuotePosts && !status.content.statusesURLs.isEmpty { return false } @@ -67,7 +68,7 @@ actor TimelineDatasource { func insert(contentOf: [Status], at: Int) { statuses.insert(contentsOf: contentOf, at: at) } - + func remove(after: Status, safeOffset: Int) { if let index = statuses.firstIndex(of: after) { let safeIndex = index + safeOffset diff --git a/Packages/Timeline/Sources/Timeline/actors/TimelineStatusFetcher.swift b/Packages/Timeline/Sources/Timeline/actors/TimelineStatusFetcher.swift index 35b8bc99..382f6f58 100644 --- a/Packages/Timeline/Sources/Timeline/actors/TimelineStatusFetcher.swift +++ b/Packages/Timeline/Sources/Timeline/actors/TimelineStatusFetcher.swift @@ -3,16 +3,22 @@ import Models import Network protocol TimelineStatusFetching: Sendable { - func fetchFirstPage(client: Client?, - timeline: TimelineFilter) async throws -> [Status] - func fetchNewPages(client: Client?, - timeline: TimelineFilter, - minId: String, - maxPages: Int) async throws -> [Status] - func fetchNextPage(client: Client?, - timeline: TimelineFilter, - lastId: String, - offset: Int) async throws -> [Status] + func fetchFirstPage( + client: Client?, + timeline: TimelineFilter + ) async throws -> [Status] + func fetchNewPages( + client: Client?, + timeline: TimelineFilter, + minId: String, + maxPages: Int + ) async throws -> [Status] + func fetchNextPage( + client: Client?, + timeline: TimelineFilter, + lastId: String, + offset: Int + ) async throws -> [Status] } enum StatusFetcherError: Error { @@ -22,42 +28,51 @@ enum StatusFetcherError: Error { struct TimelineStatusFetcher: TimelineStatusFetching { func fetchFirstPage(client: Client?, timeline: TimelineFilter) async throws -> [Status] { guard let client = client else { throw StatusFetcherError.noClientAvailable } - return try await client.get(endpoint: timeline.endpoint(sinceId: nil, - maxId: nil, - minId: nil, - offset: 0, - limit: 40)) + return try await client.get( + endpoint: timeline.endpoint( + sinceId: nil, + maxId: nil, + minId: nil, + offset: 0, + limit: 40)) } - - func fetchNewPages(client: Client?, timeline: TimelineFilter, minId: String, maxPages: Int) async throws -> [Status] { + + func fetchNewPages(client: Client?, timeline: TimelineFilter, minId: String, maxPages: Int) + async throws -> [Status] + { guard let client = client else { throw StatusFetcherError.noClientAvailable } var allStatuses: [Status] = [] var latestMinId = minId for _ in 1...maxPages { if Task.isCancelled { break } - - let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint( - sinceId: nil, - maxId: nil, - minId: latestMinId, - offset: nil, - limit: 40 - )) - + + let newStatuses: [Status] = try await client.get( + endpoint: timeline.endpoint( + sinceId: nil, + maxId: nil, + minId: latestMinId, + offset: nil, + limit: 40 + )) + if newStatuses.isEmpty { break } - + allStatuses.insert(contentsOf: newStatuses, at: 0) latestMinId = newStatuses.first?.id ?? latestMinId } return allStatuses } - - func fetchNextPage(client: Client?, timeline: TimelineFilter, lastId: String, offset: Int) async throws -> [Status] { + + func fetchNextPage(client: Client?, timeline: TimelineFilter, lastId: String, offset: Int) + async throws -> [Status] + { guard let client = client else { throw StatusFetcherError.noClientAvailable } - return try await client.get(endpoint: timeline.endpoint(sinceId: nil, - maxId: lastId, - minId: nil, - offset: offset, - limit: 40)) + return try await client.get( + endpoint: timeline.endpoint( + sinceId: nil, + maxId: lastId, + minId: nil, + offset: offset, + limit: 40)) } } diff --git a/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift b/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift index 37ed8217..3e9dbbd3 100644 --- a/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift +++ b/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift @@ -1,19 +1,23 @@ +import Foundation import Models import Network import Testing -import Foundation + @testable import Timeline @Suite("Timeline Filter Tests") struct TimelineFilterTests { - @Test("All timeline filter can be decoded and encoded", - arguments: [TimelineFilter.home, - TimelineFilter.local, - TimelineFilter.federated, - TimelineFilter.remoteLocal(server: "me.dm", filter: .local), - TimelineFilter.tagGroup(title: "test", tags: ["test"], symbolName: nil), - TimelineFilter.tagGroup(title: "test", tags: ["test"], symbolName: "test"), - TimelineFilter.hashtag(tag: "test", accountId: nil)]) + @Test( + "All timeline filter can be decoded and encoded", + arguments: [ + TimelineFilter.home, + TimelineFilter.local, + TimelineFilter.federated, + TimelineFilter.remoteLocal(server: "me.dm", filter: .local), + TimelineFilter.tagGroup(title: "test", tags: ["test"], symbolName: nil), + TimelineFilter.tagGroup(title: "test", tags: ["test"], symbolName: "test"), + TimelineFilter.hashtag(tag: "test", accountId: nil), + ]) func timelineCanEncodeAndDecode(filter: TimelineFilter) { #expect(testCodableOn(filter: filter)) } diff --git a/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift b/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift index 9bce7b9c..04fc7396 100644 --- a/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift +++ b/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift @@ -1,8 +1,9 @@ import Models import Network -@testable import Timeline -import XCTest import Testing +import XCTest + +@testable import Timeline @MainActor @Suite("Timeline View Model tests") @@ -16,7 +17,7 @@ struct Tests { subject.timelineTask?.cancel() return subject } - + /* @Test func streamEventInsertNewStatus() async throws { @@ -31,7 +32,7 @@ struct Tests { #expect(count == 2) } */ - + @Test func streamEventInsertDuplicateStatus() async throws { let subject = makeSubject() @@ -69,33 +70,34 @@ struct Tests { await subject.datasource.append(status) var count = await subject.datasource.count() #expect(count == 1) - status = .init(id: status.id, - content: .init(stringValue: "test"), - account: status.account, - createdAt: status.createdAt, - editedAt: status.editedAt, - reblog: status.reblog, - mediaAttachments: status.mediaAttachments, - mentions: status.mentions, - repliesCount: status.repliesCount, - reblogsCount: status.reblogsCount, - favouritesCount: status.favouritesCount, - card: status.card, - favourited: status.favourited, - reblogged: status.reblogged, - pinned: status.pinned, - bookmarked: status.bookmarked, - emojis: status.emojis, - url: status.url, - application: status.application, - inReplyToId: status.inReplyToId, - inReplyToAccountId: status.inReplyToAccountId, - visibility: status.visibility, - poll: status.poll, - spoilerText: status.spoilerText, - filtered: status.filtered, - sensitive: status.sensitive, - language: status.language) + status = .init( + id: status.id, + content: .init(stringValue: "test"), + account: status.account, + createdAt: status.createdAt, + editedAt: status.editedAt, + reblog: status.reblog, + mediaAttachments: status.mediaAttachments, + mentions: status.mentions, + repliesCount: status.repliesCount, + reblogsCount: status.reblogsCount, + favouritesCount: status.favouritesCount, + card: status.card, + favourited: status.favourited, + reblogged: status.reblogged, + pinned: status.pinned, + bookmarked: status.bookmarked, + emojis: status.emojis, + url: status.url, + application: status.application, + inReplyToId: status.inReplyToId, + inReplyToAccountId: status.inReplyToAccountId, + visibility: status.visibility, + poll: status.poll, + spoilerText: status.spoilerText, + filtered: status.filtered, + sensitive: status.sensitive, + language: status.language) await subject.handleEvent(event: StreamEventStatusUpdate(status: status)) let statuses = await subject.datasource.get() count = await subject.datasource.count()