From 7f6419ebae9cd56f2fd912e58964dd61521928bb Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Tue, 17 Jan 2023 11:36:01 +0100 Subject: [PATCH] Swiftformat --- .swiftformat | 1 + IceCubesApp.xcodeproj/project.pbxproj | 4 +- IceCubesApp/App/AppRouteur.swift | 23 ++- IceCubesApp/App/IceCubesApp.swift | 99 +++++------ IceCubesApp/App/QuickLookRepresentable.swift | 37 +++-- IceCubesApp/App/SafariRouteur.swift | 136 ++++++++-------- IceCubesApp/App/SideBarView.swift | 10 +- IceCubesApp/App/Tabs/ExploreTab.swift | 15 +- IceCubesApp/App/Tabs/MessagesTab.swift | 13 +- IceCubesApp/App/Tabs/NotificationTab.swift | 8 +- .../App/Tabs/Settings/AddAccountsView.swift | 34 ++-- .../Tabs/Settings/DisplaySettingsView.swift | 16 +- .../App/Tabs/Settings/IconSelectorView.swift | 16 +- .../App/Tabs/Settings/InstanceInfoView.swift | 14 +- .../Tabs/Settings/PushNotificationsView.swift | 22 +-- .../App/Tabs/Settings/SettingsTab.swift | 34 ++-- .../App/Tabs/Settings/SupportAppView.swift | 22 +-- IceCubesApp/App/Tabs/Tabs.swift | 17 +- .../Tabs/Timeline/AddRemoteTimelineVIew.swift | 24 +-- .../App/Tabs/Timeline/TimelineTab.swift | 29 ++-- .../NotificationService.swift | 46 +++--- .../NotificationServiceSupport.swift | 37 ++--- IceCubesShareExtension/Info.plist | 12 +- .../ShareViewController.swift | 32 ++-- Packages/Account/Package.swift | 9 +- .../Account/AccountDetailHeaderView.swift | 42 ++--- .../Sources/Account/AccountDetailView.swift | 54 +++--- .../Account/AccountDetailViewModel.swift | 91 ++++++----- .../AccountsLIst/AccountsListRow.swift | 18 +- .../AccountsLIst/AccountsListView.swift | 28 ++-- .../AccountsLIst/AccountsListViewModel.swift | 33 ++-- .../Account/Edit/EditAccountView.swift | 24 +-- .../Account/Edit/EditAccountViewModel.swift | 31 ++-- .../Sources/Account/Follow/FollowButton.swift | 20 +-- .../Tests/AccountTests/AccountTests.swift | 14 +- Packages/AppAccount/Package.swift | 6 +- .../Sources/AppAccount/AppAccount.swift | 24 +-- .../Sources/AppAccount/AppAccountView.swift | 11 +- .../AppAccount/AppAccountViewModel.swift | 12 +- .../AppAccount/AppAccountsManager.swift | 25 +-- .../AppAccount/AppAccountsSelectorView.swift | 27 +-- Packages/Conversations/Package.swift | 7 +- .../List/ConversationsListRow.swift | 16 +- .../List/ConversationsListView.swift | 20 +-- .../List/ConversationsListViewModel.swift | 14 +- Packages/DesignSystem/Package.swift | 11 +- .../Sources/DesignSystem/AccountExt.swift | 14 +- .../Sources/DesignSystem/ColorSet.swift | 49 +++--- .../Sources/DesignSystem/DesignSystem.swift | 10 +- .../DesignSystem/Resources/Colors.swift | 39 +++-- .../Sources/DesignSystem/Theme.swift | 40 ++--- .../Sources/DesignSystem/ThemeApplier.swift | 114 ++++++------- .../DesignSystem/Views/AvatarView.swift | 42 ++--- .../DesignSystem/Views/EmojiText.swift | 8 +- .../DesignSystem/Views/EmptyView.swift | 4 +- .../DesignSystem/Views/ErrorView.swift | 6 +- .../Views/ScrollViewOffsetReader.swift | 4 +- .../Views/StatusEditorToolbarItem.swift | 10 +- .../DesignSystem/Views/TagRowView.swift | 8 +- .../DesignSystem/Views/ThemePreviewView.swift | 26 ++- Packages/Env/Package.swift | 8 +- Packages/Env/Sources/Env/CurrentAccount.swift | 38 ++--- .../Env/Sources/Env/CurrentInstance.swift | 14 +- Packages/Env/Sources/Env/Ext/AppStorage.swift | 2 +- .../Env/Sources/Env/PreferredBrowser.swift | 4 +- .../Env/PushNotificationsService.swift | 80 ++++----- Packages/Env/Sources/Env/QuickLook.swift | 10 +- Packages/Env/Sources/Env/Routeur.swift | 27 +-- Packages/Env/Sources/Env/StreamWatcher.swift | 32 ++-- .../Env/Sources/Env/UserPreferences.swift | 20 +-- Packages/Explore/Package.swift | 9 +- .../Explore/Sources/Explore/ExploreView.swift | 69 ++++---- .../Sources/Explore/ExploreViewModel.swift | 40 ++--- Packages/Lists/Package.swift | 9 +- .../AddAccounts/ListAddAccountView.swift | 11 +- .../AddAccounts/ListAddAccountViewModel.swift | 14 +- .../Sources/Lists/Edit/ListEditView.swift | 12 +- .../Lists/Edit/ListEditViewModel.swift | 16 +- Packages/Models/Package.swift | 9 +- Packages/Models/Sources/Models/Account.swift | 13 +- .../Sources/Models/Alias/HTMLString.swift | 20 +-- .../Sources/Models/Alias/ServerDate.swift | 11 +- Packages/Models/Sources/Models/App/App.swift | 2 +- Packages/Models/Sources/Models/Card.swift | 2 +- .../Models/Sources/Models/Conversation.swift | 4 +- Packages/Models/Sources/Models/Emoji.swift | 5 +- Packages/Models/Sources/Models/Filter.swift | 4 +- Packages/Models/Sources/Models/Instance.swift | 10 +- .../Sources/Models/InstanceSocial.swift | 1 + .../Models/MastodonPushNotification.swift | 7 +- .../Sources/Models/MediaAttachement.swift | 10 +- .../Models/Sources/Models/Notification.swift | 9 +- Packages/Models/Sources/Models/Poll.swift | 4 +- .../Sources/Models/PushSubscription.swift | 2 +- .../Sources/Models/Relationshionship.swift | 4 +- .../Models/Sources/Models/SearchResults.swift | 2 +- .../Sources/Models/ServerPreferences.swift | 4 +- Packages/Models/Sources/Models/Status.swift | 14 +- .../Sources/Models/Stream/StreamEvent.swift | 2 +- .../Sources/Models/Stream/StreamMessage.swift | 3 +- Packages/Models/Sources/Models/Tag.swift | 20 +-- .../Tests/ModelsTests/ModelsTests.swift | 14 +- Packages/Network/Package.swift | 11 +- Packages/Network/Sources/Network/Client.swift | 78 ++++----- .../Sources/Network/Endpoint/Accounts.swift | 22 +-- .../Sources/Network/Endpoint/Apps.swift | 6 +- .../Network/Endpoint/Conversations.swift | 4 +- .../Sources/Network/Endpoint/Endpoint.swift | 4 +- .../Sources/Network/Endpoint/Instances.swift | 4 +- .../Sources/Network/Endpoint/Lists.swift | 6 +- .../Sources/Network/Endpoint/Media.swift | 4 +- .../Network/Endpoint/Notifications.swift | 6 +- .../Sources/Network/Endpoint/Oauth.swift | 8 +- .../Sources/Network/Endpoint/Polls.swift | 10 +- .../Sources/Network/Endpoint/Push.swift | 4 +- .../Sources/Network/Endpoint/Search.swift | 6 +- .../Sources/Network/Endpoint/Statuses.swift | 31 ++-- .../Sources/Network/Endpoint/Streaming.swift | 4 +- .../Sources/Network/Endpoint/Tags.swift | 10 +- .../Sources/Network/Endpoint/Timelines.swift | 10 +- .../Sources/Network/Endpoint/Trends.swift | 4 +- .../Network/InstanceSocialClient.swift | 10 +- .../Network/Sources/Network/LinkHandler.swift | 2 +- .../Sources/Network/OpenAIClient.swift | 30 ++-- .../Tests/NetworkTests/NetworkTests.swift | 14 +- Packages/Notifications/Package.swift | 9 +- .../Notifications/NotificationRowView.swift | 52 +++--- .../Notifications/NotificationTypeExt.swift | 4 +- .../Notifications/NotificationsListView.swift | 34 ++-- .../NotificationsViewModel.swift | 44 ++--- Packages/Status/Package.swift | 7 +- .../Status/Detail/StatusDetailVIew.swift | 22 +-- .../Status/Detail/StatusDetailViewModel.swift | 30 ++-- .../Components/StatusEditorAIPrompts.swift | 6 +- .../StatusEditorAccessoryView.swift | 39 +++-- .../StatusEditorAutoCompleteView.swift | 9 +- .../StatusEditorMediaEditView.swift | 13 +- .../Components/StatusEditorMediaView.swift | 25 ++- .../Components/StatusEditorPollView.swift | 4 +- .../StatusEditorUTTypeSupported.swift | 13 +- .../Status/Editor/StatusEditorView.swift | 55 +++---- .../Status/Editor/StatusEditorViewModel.swift | 154 +++++++++--------- .../Editor/StatusEditorViewModelMode.swift | 20 +-- .../Status/Embed/StatusEmbededView.swift | 16 +- .../Sources/Status/Ext/Visibility.swift | 12 +- .../Sources/Status/List/StatusesFetcher.swift | 3 +- .../Status/List/StatusesListView.swift | 14 +- .../Status/Media/VideoPlayerView.swift | 12 +- .../Sources/Status/Poll/StatusPollView.swift | 25 +-- .../Status/Poll/StatusPollViewModel.swift | 18 +- .../Status/Row/StatusActionsView.swift | 32 ++-- .../Sources/Status/Row/StatusCardView.swift | 12 +- .../Status/Row/StatusMediaPreviewView.swift | 80 ++++----- .../Status/Row/StatusRowContextMenu.swift | 26 +-- .../Sources/Status/Row/StatusRowView.swift | 57 ++++--- .../Status/Row/StatusRowViewModel.swift | 74 +++++---- Packages/Timeline/Package.swift | 12 +- .../Sources/Timeline/TimelineFilter.swift | 12 +- .../Sources/Timeline/TimelineView.swift | 26 +-- .../Sources/Timeline/TimelineViewModel.swift | 47 +++--- .../Tests/TimelineTests/TimelineTests.swift | 14 +- 161 files changed, 1777 insertions(+), 1746 deletions(-) create mode 100644 .swiftformat diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 00000000..322474e8 --- /dev/null +++ b/.swiftformat @@ -0,0 +1 @@ +--indent 2 diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index f9055bc8..3e5d33fa 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -644,7 +644,7 @@ CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 730; DEVELOPMENT_TEAM = Z6P74P6T99; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = IceCubesShareExtension/Info.plist; @@ -674,7 +674,7 @@ CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 730; DEVELOPMENT_TEAM = Z6P74P6T99; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = IceCubesShareExtension/Info.plist; diff --git a/IceCubesApp/App/AppRouteur.swift b/IceCubesApp/App/AppRouteur.swift index ecebb215..2388f746 100644 --- a/IceCubesApp/App/AppRouteur.swift +++ b/IceCubesApp/App/AppRouteur.swift @@ -1,16 +1,16 @@ +import Account +import AppAccount +import DesignSystem +import Env +import Lists +import Status import SwiftUI import Timeline -import Account -import Env -import Status -import DesignSystem -import Lists -import AppAccount @MainActor extension View { func withAppRouteur() -> some View { - self.navigationDestination(for: RouteurDestinations.self) { destination in + navigationDestination(for: RouteurDestinations.self) { destination in switch destination { case let .accountDetail(id): AccountDetailView(accountId: id) @@ -35,9 +35,9 @@ extension View { } } } - + func withSheetDestinations(sheetDestinations: Binding) -> some View { - self.sheet(item: sheetDestinations) { destination in + sheet(item: sheetDestinations) { destination in switch destination { case let .replyToStatusEditor(status): StatusEditorView(mode: .replyTo(status: status)) @@ -69,10 +69,9 @@ extension View { } } } - + func withEnvironments() -> some View { - self - .environmentObject(CurrentAccount.shared) + environmentObject(CurrentAccount.shared) .environmentObject(UserPreferences.shared) .environmentObject(CurrentInstance.shared) .environmentObject(Theme.shared) diff --git a/IceCubesApp/App/IceCubesApp.swift b/IceCubesApp/App/IceCubesApp.swift index d8d9792d..5da77983 100644 --- a/IceCubesApp/App/IceCubesApp.swift +++ b/IceCubesApp/App/IceCubesApp.swift @@ -1,18 +1,18 @@ -import SwiftUI -import AVFoundation -import Timeline -import Network -import KeychainSwift -import Env -import DesignSystem -import RevenueCat -import AppAccount import Account +import AppAccount +import AVFoundation +import DesignSystem +import Env +import KeychainSwift +import Network +import RevenueCat +import SwiftUI +import Timeline @main -struct IceCubesApp: App { +struct IceCubesApp: App { @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate - + @Environment(\.scenePhase) private var scenePhase @StateObject private var appAccountsManager = AppAccountsManager.shared @StateObject private var currentInstance = CurrentInstance.shared @@ -21,38 +21,38 @@ struct IceCubesApp: App { @StateObject private var watcher = StreamWatcher() @StateObject private var quickLook = QuickLook() @StateObject private var theme = Theme.shared - + @State private var selectedTab: Tab = .timeline @State private var selectSidebarItem: Tab? = .timeline @State private var popToRootTab: Tab = .other @State private var sideBarLoadedTabs: [Tab] = [] - + private var availableTabs: [Tab] { appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab() } - + var body: some Scene { WindowGroup { appView - .applyTheme(theme) - .onAppear { - setNewClientsInEnv(client: appAccountsManager.currentClient) - setupRevenueCat() - refreshPushSubs() - } - .environmentObject(appAccountsManager) - .environmentObject(appAccountsManager.currentClient) - .environmentObject(quickLook) - .environmentObject(currentAccount) - .environmentObject(currentInstance) - .environmentObject(userPreferences) - .environmentObject(theme) - .environmentObject(watcher) - .environmentObject(PushNotificationsService.shared) - .sheet(item: $quickLook.url, content: { url in - QuickLookPreview(selectedURL: url, urls: quickLook.urls) - .edgesIgnoringSafeArea(.bottom) - }) + .applyTheme(theme) + .onAppear { + setNewClientsInEnv(client: appAccountsManager.currentClient) + setupRevenueCat() + refreshPushSubs() + } + .environmentObject(appAccountsManager) + .environmentObject(appAccountsManager.currentClient) + .environmentObject(quickLook) + .environmentObject(currentAccount) + .environmentObject(currentInstance) + .environmentObject(userPreferences) + .environmentObject(theme) + .environmentObject(watcher) + .environmentObject(PushNotificationsService.shared) + .sheet(item: $quickLook.url, content: { url in + QuickLookPreview(selectedURL: url, urls: quickLook.urls) + .edgesIgnoringSafeArea(.bottom) + }) } .onChange(of: scenePhase) { scenePhase in handleScenePhase(scenePhase: scenePhase) @@ -64,7 +64,7 @@ struct IceCubesApp: App { } } } - + @ViewBuilder private var appView: some View { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { @@ -73,14 +73,14 @@ struct IceCubesApp: App { tabBarView } } - + private func badgeFor(tab: Tab) -> Int { if tab == .notifications && selectedTab != tab { return watcher.unreadNotificationsCount + userPreferences.pushNotificationsCount } return 0 } - + private var sidebarView: some View { SideBarView(selectedTab: $selectedTab, popToRootTab: $popToRootTab, @@ -107,7 +107,7 @@ struct IceCubesApp: App { } } } - + private var tabBarView: some View { TabView(selection: .init(get: { selectedTab @@ -132,14 +132,14 @@ struct IceCubesApp: App { } } } - + private func setNewClientsInEnv(client: Client) { currentAccount.setClient(client: client) currentInstance.setClient(client: client) userPreferences.setClient(client: client) watcher.setClient(client: client) } - + private func handleScenePhase(scenePhase: ScenePhase) { switch scenePhase { case .background: @@ -156,33 +156,34 @@ struct IceCubesApp: App { break } } - + private func setupRevenueCat() { Purchases.logLevel = .error Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi") } - + private func refreshPushSubs() { PushNotificationsService.shared.requestPushNotifications() } } class AppDelegate: NSObject, UIApplicationDelegate { - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + func application(_: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool + { try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers) return true } - - func application(_ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + + func application(_: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) + { PushNotificationsService.shared.pushToken = deviceToken Task { await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts) await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts) } } - - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - } + + func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {} } diff --git a/IceCubesApp/App/QuickLookRepresentable.swift b/IceCubesApp/App/QuickLookRepresentable.swift index 56b411bb..1534600b 100644 --- a/IceCubesApp/App/QuickLookRepresentable.swift +++ b/IceCubesApp/App/QuickLookRepresentable.swift @@ -1,6 +1,6 @@ -import UIKit -import SwiftUI import QuickLook +import SwiftUI +import UIKit extension URL: Identifiable { public var id: String { @@ -11,7 +11,7 @@ extension URL: Identifiable { struct QuickLookPreview: UIViewControllerRepresentable { let selectedURL: URL let urls: [URL] - + func makeUIViewController(context: Context) -> UINavigationController { let controller = AppQLPreviewCpntroller() controller.dataSource = context.coordinator @@ -19,30 +19,31 @@ struct QuickLookPreview: UIViewControllerRepresentable { let nav = UINavigationController(rootViewController: controller) return nav } - + func updateUIViewController( - _ uiViewController: UINavigationController, context: Context) {} - + _: UINavigationController, context _: Context + ) {} + func makeCoordinator() -> Coordinator { return Coordinator(parent: self) } - + class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate { let parent: QuickLookPreview - + init(parent: QuickLookPreview) { self.parent = parent } - - func numberOfPreviewItems(in controller: QLPreviewController) -> Int { + + func numberOfPreviewItems(in _: QLPreviewController) -> Int { return parent.urls.count } - - func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { + + func previewController(_: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { return parent.urls[index] as QLPreviewItem } - - func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode { + + func previewController(_: QLPreviewController, editingModeFor _: QLPreviewItem) -> QLPreviewItemEditingMode { .createCopy } } @@ -52,22 +53,22 @@ class AppQLPreviewCpntroller: QLPreviewController { private var closeButton: UIBarButtonItem { .init(title: "Done", style: .plain, target: self, action: #selector(onCloseButton)) } - + override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = closeButton } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) navigationItem.rightBarButtonItem = closeButton } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() navigationItem.rightBarButtonItem = closeButton } - + @objc private func onCloseButton() { dismiss(animated: true) } diff --git a/IceCubesApp/App/SafariRouteur.swift b/IceCubesApp/App/SafariRouteur.swift index b47e21ad..c79658a5 100644 --- a/IceCubesApp/App/SafariRouteur.swift +++ b/IceCubesApp/App/SafariRouteur.swift @@ -1,85 +1,85 @@ -import SwiftUI -import SafariServices -import Env import DesignSystem +import Env +import SafariServices +import SwiftUI extension View { - func withSafariRouteur() -> some View { - modifier(SafariRouteur()) - } + func withSafariRouteur() -> some View { + modifier(SafariRouteur()) + } } private struct SafariRouteur: ViewModifier { - @EnvironmentObject private var theme: Theme - @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var routeurPath: RouterPath - - @State private var safari: SFSafariViewController? - - func body(content: Content) -> some View { - content - .environment(\.openURL, OpenURLAction { url in - routeurPath.handle(url: url) - }) - .onAppear { - routeurPath.urlHandler = { url in - guard preferences.preferredBrowser == .inAppSafari else { return .systemAction } - // SFSafariViewController only supports initial URLs with http:// or https:// schemes. - guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else { - return .systemAction - } - - let safari = SFSafariViewController(url: url) - safari.preferredBarTintColor = UIColor(theme.primaryBackgroundColor) - safari.preferredControlTintColor = UIColor(theme.tintColor) - - self.safari = safari - return .handled - } - } - .background { - SafariPresenter(safari: safari) - } - } - - struct SafariPresenter: UIViewRepresentable { - var safari: SFSafariViewController? - - func makeUIView(context: Context) -> UIView { - let view = UIView(frame: .zero) - view.isHidden = true - view.isUserInteractionEnabled = false - return view - } - - func updateUIView(_ uiView: UIView, context: Context) { - guard let safari = safari, let viewController = uiView.findTopViewController() else { return } - viewController.present(safari, animated: true) + @EnvironmentObject private var theme: Theme + @EnvironmentObject private var preferences: UserPreferences + @EnvironmentObject private var routeurPath: RouterPath + + @State private var safari: SFSafariViewController? + + func body(content: Content) -> some View { + content + .environment(\.openURL, OpenURLAction { url in + routeurPath.handle(url: url) + }) + .onAppear { + routeurPath.urlHandler = { url in + guard preferences.preferredBrowser == .inAppSafari else { return .systemAction } + // SFSafariViewController only supports initial URLs with http:// or https:// schemes. + guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else { + return .systemAction + } + + let safari = SFSafariViewController(url: url) + safari.preferredBarTintColor = UIColor(theme.primaryBackgroundColor) + safari.preferredControlTintColor = UIColor(theme.tintColor) + + self.safari = safari + return .handled } + } + .background { + SafariPresenter(safari: safari) + } + } + + struct SafariPresenter: UIViewRepresentable { + var safari: SFSafariViewController? + + func makeUIView(context _: Context) -> UIView { + let view = UIView(frame: .zero) + view.isHidden = true + view.isUserInteractionEnabled = false + return view } + + func updateUIView(_ uiView: UIView, context _: Context) { + guard let safari = safari, let viewController = uiView.findTopViewController() else { return } + viewController.present(safari, animated: true) + } + } } private extension UIView { - func findTopViewController() -> UIViewController? { - if let nextResponder = self.next as? UIViewController { - return nextResponder.topViewController() - } else if let nextResponder = self.next as? UIView { - return nextResponder.findTopViewController() - } else { - return nil - } + func findTopViewController() -> UIViewController? { + if let nextResponder = next as? UIViewController { + return nextResponder.topViewController() + } else if let nextResponder = next as? UIView { + return nextResponder.findTopViewController() + } else { + return nil } + } } private extension UIViewController { - func topViewController() -> UIViewController? { - if let nvc = self as? UINavigationController { - return nvc.visibleViewController?.topViewController() - } else if let tbc = self as? UITabBarController, let selected = tbc.selectedViewController { - return selected.topViewController() - } else if let presented = self.presentedViewController { - return presented.topViewController() - } - return self + func topViewController() -> UIViewController? { + if let nvc = self as? UINavigationController { + return nvc.visibleViewController?.topViewController() + } else if let tbc = self as? UITabBarController, let selected = tbc.selectedViewController { + return selected.topViewController() + } else if let presented = presentedViewController { + return presented.topViewController() } + return self + } } diff --git a/IceCubesApp/App/SideBarView.swift b/IceCubesApp/App/SideBarView.swift index 9d3f0118..720bd0c3 100644 --- a/IceCubesApp/App/SideBarView.swift +++ b/IceCubesApp/App/SideBarView.swift @@ -1,18 +1,18 @@ -import SwiftUI -import Env import Account -import DesignSystem import AppAccount +import DesignSystem +import Env +import SwiftUI struct SideBarView: View { @EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var theme: Theme - + @Binding var selectedTab: Tab @Binding var popToRootTab: Tab var tabs: [Tab] @ViewBuilder var content: () -> Content - + var body: some View { HStack(spacing: 0) { VStack(alignment: .center) { diff --git a/IceCubesApp/App/Tabs/ExploreTab.swift b/IceCubesApp/App/Tabs/ExploreTab.swift index 503679b1..e1da2c7f 100644 --- a/IceCubesApp/App/Tabs/ExploreTab.swift +++ b/IceCubesApp/App/Tabs/ExploreTab.swift @@ -1,11 +1,10 @@ -import SwiftUI -import Env -import Models -import Shimmer -import Explore -import Env -import Network import AppAccount +import Env +import Explore +import Models +import Network +import Shimmer +import SwiftUI struct ExploreTab: View { @EnvironmentObject private var preferences: UserPreferences @@ -13,7 +12,7 @@ struct ExploreTab: View { @EnvironmentObject private var client: Client @StateObject private var routeurPath = RouterPath() @Binding var popToRootTab: Tab - + var body: some View { NavigationStack(path: $routeurPath.path) { ExploreView() diff --git a/IceCubesApp/App/Tabs/MessagesTab.swift b/IceCubesApp/App/Tabs/MessagesTab.swift index 69ae1815..13f6f10d 100644 --- a/IceCubesApp/App/Tabs/MessagesTab.swift +++ b/IceCubesApp/App/Tabs/MessagesTab.swift @@ -1,12 +1,11 @@ -import SwiftUI -import Env -import Network import Account -import Models -import Shimmer +import AppAccount import Conversations import Env -import AppAccount +import Models +import Network +import Shimmer +import SwiftUI struct MessagesTab: View { @EnvironmentObject private var watcher: StreamWatcher @@ -14,7 +13,7 @@ struct MessagesTab: View { @EnvironmentObject private var currentAccount: CurrentAccount @StateObject private var routeurPath = RouterPath() @Binding var popToRootTab: Tab - + var body: some View { NavigationStack(path: $routeurPath.path) { ConversationsListView() diff --git a/IceCubesApp/App/Tabs/NotificationTab.swift b/IceCubesApp/App/Tabs/NotificationTab.swift index dca60435..6b56fc44 100644 --- a/IceCubesApp/App/Tabs/NotificationTab.swift +++ b/IceCubesApp/App/Tabs/NotificationTab.swift @@ -1,9 +1,9 @@ -import SwiftUI -import Timeline +import AppAccount import Env import Network import Notifications -import AppAccount +import SwiftUI +import Timeline struct NotificationsTab: View { @EnvironmentObject private var client: Client @@ -12,7 +12,7 @@ struct NotificationsTab: View { @EnvironmentObject private var userPreferences: UserPreferences @StateObject private var routeurPath = RouterPath() @Binding var popToRootTab: Tab - + var body: some View { NavigationStack(path: $routeurPath.path) { NotificationsListView() diff --git a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift index 2c9562ef..b1743ecc 100644 --- a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift +++ b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift @@ -1,23 +1,23 @@ -import SwiftUI -import Network -import Models -import Env -import DesignSystem -import NukeUI -import Shimmer import AppAccount import Combine +import DesignSystem +import Env +import Models +import Network +import NukeUI +import Shimmer +import SwiftUI struct AddAccountView: View { @Environment(\.dismiss) private var dismiss @Environment(\.scenePhase) private var scenePhase - + @EnvironmentObject private var appAccountsManager: AppAccountsManager @EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var currentInstance: CurrentInstance @EnvironmentObject private var pushNotifications: PushNotificationsService @EnvironmentObject private var theme: Theme - + @State private var instanceName: String = "" @State private var instance: Instance? @State private var isSigninIn = false @@ -26,9 +26,9 @@ struct AddAccountView: View { @State private var instanceFetchError: String? private let instanceNamePublisher = PassthroughSubject() - + @FocusState private var isInstanceURLFieldFocused: Bool - + var body: some View { NavigationStack { Form { @@ -102,7 +102,7 @@ struct AddAccountView: View { }) } } - + private var signInSection: some View { Section { Button { @@ -127,13 +127,13 @@ struct AddAccountView: View { } .listRowBackground(theme.tintColor) } - + private var instancesListView: some View { Section("Suggestions") { if instances.isEmpty { placeholderRow } 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 { self.instanceName = instance.name } label: { @@ -154,7 +154,7 @@ struct AddAccountView: View { } } } - + private var placeholderRow: some View { VStack(alignment: .leading, spacing: 4) { Text("Loading...") @@ -171,7 +171,7 @@ struct AddAccountView: View { .shimmering() .listRowBackground(theme.primaryBackgroundColor) } - + private func signIn() async { do { signInClient = .init(server: instanceName) @@ -184,7 +184,7 @@ struct AddAccountView: View { isSigninIn = false } } - + private func continueSignIn(url: URL) async { guard let client = signInClient else { isSigninIn = false diff --git a/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift b/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift index 7e8712e4..478bd088 100644 --- a/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift @@ -1,13 +1,13 @@ -import SwiftUI -import Models import DesignSystem +import Models import Status +import SwiftUI struct DisplaySettingsView: View { @EnvironmentObject private var theme: Theme - + @State private var isThemeSelectorPresented = false - + var body: some View { Form { Section("Theme") { @@ -17,7 +17,7 @@ struct DisplaySettingsView: View { ColorPicker("Secondary Background color", selection: $theme.secondaryBackgroundColor) } .listRowBackground(theme.primaryBackgroundColor) - + Section("Display") { Picker("Avatar position", selection: $theme.avatarPosition) { ForEach(Theme.AvatarPosition.allCases, id: \.rawValue) { position in @@ -34,7 +34,7 @@ struct DisplaySettingsView: View { Text(buttonStyle.description).tag(buttonStyle) } } - + Picker("Status media style", selection: $theme.statusDisplayStyle) { ForEach(Theme.StatusDisplayStyle.allCases, id: \.rawValue) { buttonStyle in Text(buttonStyle.description).tag(buttonStyle) @@ -42,7 +42,7 @@ struct DisplaySettingsView: View { } } .listRowBackground(theme.primaryBackgroundColor) - + Section { Button { theme.selectedSet = .iceCubeDark @@ -59,7 +59,7 @@ struct DisplaySettingsView: View { .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) } - + private var themeSelectorButton: some View { NavigationLink(destination: ThemePreviewView()) { HStack { diff --git a/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift b/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift index 9053a117..667214ab 100644 --- a/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift +++ b/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift @@ -1,12 +1,12 @@ -import SwiftUI import DesignSystem +import SwiftUI struct IconSelectorView: View { enum Icon: Int, CaseIterable, Identifiable { var id: String { "\(rawValue)" } - + init(string: String) { if string == Icon.primary.appIconName { self = .primary @@ -14,12 +14,12 @@ struct IconSelectorView: View { self = .init(rawValue: Int(String(string.last!))!)! } } - + case primary = 0 case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8 case alt9, alt10, alt11, alt12, alt13, alt14 case alt15, alt16, alt17, alt18, alt19 - + var appIconName: String { switch self { case .primary: @@ -28,17 +28,17 @@ struct IconSelectorView: View { return "AppIconAlternate\(rawValue)" } } - + var iconName: String { "icon\(rawValue)" } } - + @EnvironmentObject private var theme: Theme @State private var currentIcon = UIApplication.shared.alternateIconName ?? Icon.primary.appIconName - + private let columns = [GridItem(.adaptive(minimum: 125, maximum: 1024))] - + var body: some View { ScrollView { VStack(alignment: .leading) { diff --git a/IceCubesApp/App/Tabs/Settings/InstanceInfoView.swift b/IceCubesApp/App/Tabs/Settings/InstanceInfoView.swift index e6c5dd71..52fe91a0 100644 --- a/IceCubesApp/App/Tabs/Settings/InstanceInfoView.swift +++ b/IceCubesApp/App/Tabs/Settings/InstanceInfoView.swift @@ -1,13 +1,13 @@ -import SwiftUI -import Models import DesignSystem +import Models import NukeUI +import SwiftUI struct InstanceInfoView: View { @EnvironmentObject private var theme: Theme - + let instance: Instance - + var body: some View { Form { InstanceInfoSection(instance: instance) @@ -20,9 +20,9 @@ struct InstanceInfoView: View { public struct InstanceInfoSection: View { @EnvironmentObject private var theme: Theme - + let instance: Instance - + public var body: some View { Section("Instance info") { LabeledContent("Name", value: instance.title) @@ -34,7 +34,7 @@ public struct InstanceInfoSection: View { LabeledContent("Domains", value: "\(instance.stats.domainCount)") } .listRowBackground(theme.primaryBackgroundColor) - + Section("Instance rules") { ForEach(instance.rules) { rule in Text(rule.text) diff --git a/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift b/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift index 009f06ea..621a34cc 100644 --- a/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift +++ b/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift @@ -1,19 +1,19 @@ -import SwiftUI -import Models -import DesignSystem -import NukeUI -import Network -import UserNotifications -import Env import AppAccount +import DesignSystem +import Env +import Models +import Network +import NukeUI +import SwiftUI +import UserNotifications struct PushNotificationsView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var appAccountsManager: AppAccountsManager @EnvironmentObject private var pushNotifications: PushNotificationsService - + @State private var subscriptions: [PushSubscription] = [] - + var body: some View { Form { Section { @@ -24,7 +24,7 @@ struct PushNotificationsView: View { Text("Receive push notifications on new activities") } .listRowBackground(theme.primaryBackgroundColor) - + if pushNotifications.isPushEnabled { Section { Toggle(isOn: $pushNotifications.isMentionNotificationEnabled) { @@ -87,7 +87,7 @@ struct PushNotificationsView: View { updateSubscriptions() } } - + private func updateSubscriptions() { Task { await pushNotifications.updateSubscriptions(accounts: appAccountsManager.pushAccounts) diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index db5f2554..1d5ecca6 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -1,11 +1,11 @@ +import Account +import AppAccount +import DesignSystem +import Env +import Models +import Network import SwiftUI import Timeline -import Env -import Network -import Account -import Models -import DesignSystem -import AppAccount struct SettingsTabs: View { @EnvironmentObject private var pushNotifications: PushNotificationsService @@ -14,13 +14,13 @@ struct SettingsTabs: View { @EnvironmentObject private var currentInstance: CurrentInstance @EnvironmentObject private var appAccountsManager: AppAccountsManager @EnvironmentObject private var theme: Theme - + @StateObject private var routeurPath = RouterPath() - + @State private var addAccountSheetPresented = false - + @Binding var popToRootTab: Tab - + var body: some View { NavigationStack(path: $routeurPath.path) { Form { @@ -52,7 +52,7 @@ struct SettingsTabs: View { } } } - + private var accountsSection: some View { Section("Accounts") { ForEach(appAccountsManager.availableAccounts) { account in @@ -73,7 +73,7 @@ struct SettingsTabs: View { } .listRowBackground(theme.primaryBackgroundColor) } - + @ViewBuilder private var generalSection: some View { Section("General") { @@ -106,7 +106,7 @@ struct SettingsTabs: View { } .listRowBackground(theme.primaryBackgroundColor) } - + private var appSection: some View { Section("App") { NavigationLink(destination: IconSelectorView()) { @@ -121,19 +121,19 @@ struct SettingsTabs: View { } } } - + Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp")!) { Label("Source (GitHub link)", systemImage: "link") } .tint(theme.labelColor) - + NavigationLink(destination: SupportAppView()) { Label("Support the app", systemImage: "wand.and.stars") } } .listRowBackground(theme.primaryBackgroundColor) } - + private var addAccountButton: some View { Button { addAccountSheetPresented.toggle() @@ -144,7 +144,7 @@ struct SettingsTabs: View { AddAccountView() } } - + private var remoteLocalTimelinesView: some View { Form { ForEach(preferences.remoteLocalTimelines, id: \.self) { server in diff --git a/IceCubesApp/App/Tabs/Settings/SupportAppView.swift b/IceCubesApp/App/Tabs/Settings/SupportAppView.swift index 378b8fce..36cccc11 100644 --- a/IceCubesApp/App/Tabs/Settings/SupportAppView.swift +++ b/IceCubesApp/App/Tabs/Settings/SupportAppView.swift @@ -1,21 +1,21 @@ -import SwiftUI -import Env import DesignSystem +import Env import RevenueCat import Shimmer +import SwiftUI struct SupportAppView: View { enum Tips: String, CaseIterable { case one, two, three - + init(productId: String) { self = .init(rawValue: String(productId.split(separator: ".")[2]))! } - + var productId: String { "icecubes.tipjar.\(rawValue)" } - + var title: String { switch self { case .one: @@ -26,7 +26,7 @@ struct SupportAppView: View { return "🤯 Generous Tip" } } - + var subtitle: String { switch self { case .one: @@ -38,15 +38,15 @@ struct SupportAppView: View { } } } - + @EnvironmentObject private var theme: Theme - + @State private var loadingProducts: Bool = false @State private var products: [StoreProduct] = [] @State private var isProcessingPurchase: Bool = false @State private var purchaseSuccessDisplayed: Bool = false @State private var purchaseErrorDisplayed: Bool = false - + var body: some View { Form { Section { @@ -65,7 +65,7 @@ struct SupportAppView: View { } } .listRowBackground(theme.primaryBackgroundColor) - + Section { if loadingProducts { HStack { @@ -133,7 +133,7 @@ struct SupportAppView: View { }) .onAppear { loadingProducts = true - Purchases.shared.getProducts(Tips.allCases.map{ $0.productId }) { products in + Purchases.shared.getProducts(Tips.allCases.map { $0.productId }) { products in self.products = products.sorted(by: { $0.price < $1.price }) withAnimation { loadingProducts = false diff --git a/IceCubesApp/App/Tabs/Tabs.swift b/IceCubesApp/App/Tabs/Tabs.swift index b1bd00ca..808c12ed 100644 --- a/IceCubesApp/App/Tabs/Tabs.swift +++ b/IceCubesApp/App/Tabs/Tabs.swift @@ -1,22 +1,22 @@ -import Foundation -import Status import Account import Explore +import Foundation +import Status import SwiftUI enum Tab: Int, Identifiable, Hashable { case timeline, notifications, explore, messages, settings, other case trending, federated, local case profile - + var id: Int { rawValue } - + static func loggedOutTab() -> [Tab] { [.timeline, .settings] } - + static func loggedInTabs() -> [Tab] { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { return [.timeline, .trending, .federated, .local, .notifications, .explore, .messages, .settings] @@ -24,7 +24,7 @@ enum Tab: Int, Identifiable, Hashable { return [.timeline, .notifications, .explore, .messages, .settings] } } - + @ViewBuilder func makeContentView(popToRootTab: Binding) -> some View { switch self { @@ -48,7 +48,7 @@ enum Tab: Int, Identifiable, Hashable { EmptyView() } } - + @ViewBuilder var label: some View { switch self { @@ -72,7 +72,7 @@ enum Tab: Int, Identifiable, Hashable { EmptyView() } } - + var iconName: String { switch self { case .timeline: @@ -96,4 +96,3 @@ enum Tab: Int, Identifiable, Hashable { } } } - diff --git a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineVIew.swift b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineVIew.swift index 5b13ca68..c1660580 100644 --- a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineVIew.swift +++ b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineVIew.swift @@ -1,26 +1,26 @@ -import SwiftUI -import Network -import Models -import Env +import Combine import DesignSystem +import Env +import Models +import Network import NukeUI import Shimmer -import Combine +import SwiftUI struct AddRemoteTimelineView: View { @Environment(\.dismiss) private var dismiss - + @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var theme: Theme - + @State private var instanceName: String = "" @State private var instance: Instance? @State private var instances: [InstanceSocial] = [] private let instanceNamePublisher = PassthroughSubject() - + @FocusState private var isInstanceURLFieldFocused: Bool - + var body: some View { NavigationStack { Form { @@ -44,7 +44,7 @@ struct AddRemoteTimelineView: View { Text("Add") } .listRowBackground(theme.primaryBackgroundColor) - + instancesListView } .formStyle(.grouped) @@ -76,14 +76,14 @@ struct AddRemoteTimelineView: View { } } } - + private var instancesListView: some View { Section("Suggestions") { if instances.isEmpty { 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 { self.instanceName = instance.name } label: { diff --git a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift index 90c4f587..d3cf7dad 100644 --- a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift @@ -1,11 +1,11 @@ -import SwiftUI -import Timeline -import Env -import Network +import AppAccount import Combine import DesignSystem +import Env import Models -import AppAccount +import Network +import SwiftUI +import Timeline struct TimelineTab: View { @EnvironmentObject private var theme: Theme @@ -14,19 +14,19 @@ struct TimelineTab: View { @EnvironmentObject private var client: Client @StateObject private var routeurPath = RouterPath() @Binding var popToRootTab: Tab - + @State private var didAppear: Bool = false @State private var timeline: TimelineFilter @State private var scrollToTopSignal: Int = 0 - + private let canFilterTimeline: Bool - + init(popToRootTab: Binding, timeline: TimelineFilter? = nil) { canFilterTimeline = timeline == nil self.timeline = timeline ?? .home _popToRootTab = popToRootTab } - + var body: some View { NavigationStack(path: $routeurPath.path) { TimelineView(timeline: $timeline, scrollToTopSignal: $scrollToTopSignal) @@ -71,8 +71,7 @@ struct TimelineTab: View { .withSafariRouteur() .environmentObject(routeurPath) } - - + @ViewBuilder private var timelineFilterButton: some View { ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in @@ -93,7 +92,7 @@ struct TimelineTab: View { } } } - + if !currentAccount.tags.isEmpty { Menu("Followed Tags") { ForEach(currentAccount.tags) { tag in @@ -105,7 +104,7 @@ struct TimelineTab: View { } } } - + Menu("Local Timelines") { ForEach(preferences.remoteLocalTimelines, id: \.self) { server in Button { @@ -121,7 +120,7 @@ struct TimelineTab: View { } } } - + private var addAccountButton: some View { Button { routeurPath.presentedSheet = .addAccount @@ -129,7 +128,7 @@ struct TimelineTab: View { Image(systemName: "person.badge.plus") } } - + @ToolbarContentBuilder private var toolbarView: some ToolbarContent { if canFilterTimeline { diff --git a/IceCubesNotifications/NotificationService.swift b/IceCubesNotifications/NotificationService.swift index 599d1729..512c0386 100644 --- a/IceCubesNotifications/NotificationService.swift +++ b/IceCubesNotifications/NotificationService.swift @@ -1,71 +1,75 @@ -import UserNotifications -import KeychainSwift -import Env import CryptoKit +import Env +import KeychainSwift import Models import UIKit +import UserNotifications @MainActor class NotificationService: UNNotificationServiceExtension { - var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? - + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - + if let bestAttemptContent { let privateKey = PushNotificationsService.shared.notificationsPrivateKeyAsKey let auth = PushNotificationsService.shared.notificationsAuthKeyAsKey - + guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String, - let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) else { + let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) + else { contentHandler(bestAttemptContent) return } - + guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String, let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()), - let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) else { + let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) + else { contentHandler(bestAttemptContent) return } - + guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String, - let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64()) else { + let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64()) + else { contentHandler(bestAttemptContent) return } - + 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 { + let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) + else { contentHandler(bestAttemptContent) return } - + bestAttemptContent.title = notification.title bestAttemptContent.subtitle = "" bestAttemptContent.body = notification.body.escape() bestAttemptContent.userInfo["plaintext"] = plaintextData - bestAttemptContent.sound = UNNotificationSound.init(named: UNNotificationSoundName(rawValue: "glass.wav")) - + bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "glass.wav")) + let preferences = UserPreferences.shared preferences.pushNotificationsCount += 1 - + bestAttemptContent.badge = .init(integerLiteral: preferences.pushNotificationsCount) - + if let urlString = notification.icon, - let url = URL(string: urlString) { + 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) - + Task { if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) { if let image = UIImage(data: data) { diff --git a/IceCubesNotifications/NotificationServiceSupport.swift b/IceCubesNotifications/NotificationServiceSupport.swift index 63419d44..66bc4445 100644 --- a/IceCubesNotifications/NotificationServiceSupport.swift +++ b/IceCubesNotifications/NotificationServiceSupport.swift @@ -1,26 +1,26 @@ -import Foundation import CryptoKit +import Foundation extension NotificationService { 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 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 nonceData = nonce.withUnsafeBytes(Array.init) - + guard let sealedBox = try? AES.GCM.SealedBox(combined: nonceData + payload) else { return nil } - + var _plaintext: Data? do { _plaintext = try AES.GCM.open(sealedBox, using: key) @@ -30,20 +30,20 @@ extension NotificationService { guard let plaintext = _plaintext else { return nil } - + let paddingLength = Int(plaintext[0]) * 256 + Int(plaintext[1]) guard plaintext.count >= 2 + paddingLength else { print("1") fatalError() } let unpadded = plaintext.suffix(from: paddingLength + 2) - + return Data(unpadded) } - - static private func info(type: String, clientPublicKey: Data, serverPublicKey: Data) -> Data { + + private static func info(type: String, clientPublicKey: Data, serverPublicKey: Data) -> Data { var info = Data() - + info.append("Content-Encoding: ".data(using: .utf8)!) info.append(type.data(using: .utf8)!) info.append(0) @@ -55,32 +55,29 @@ extension NotificationService { info.append(0) info.append(65) info.append(serverPublicKey) - + return info } } extension String { func escape() -> String { - return self - .replacingOccurrences(of: "&", with: "&") + return replacingOccurrences(of: "&", with: "&") .replacingOccurrences(of: "<", with: "<") .replacingOccurrences(of: ">", with: ">") .replacingOccurrences(of: """, with: "\"") .replacingOccurrences(of: "'", with: "'") .replacingOccurrences(of: "'", with: "’") - } - + func URLSafeBase64ToBase64() -> String { var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") let countMod4 = count % 4 - + if countMod4 != 0 { base64.append(String(repeating: "=", count: 4 - countMod4)) } - + return base64 } } - diff --git a/IceCubesShareExtension/Info.plist b/IceCubesShareExtension/Info.plist index 9cf003a8..48ab9005 100644 --- a/IceCubesShareExtension/Info.plist +++ b/IceCubesShareExtension/Info.plist @@ -8,14 +8,14 @@ NSExtensionActivationRule - NSExtensionActivationSupportsText - - NSExtensionActivationSupportsWebURLWithMaxCount - 1 - NSExtensionActivationSupportsWebPageWithMaxCount - 1 NSExtensionActivationSupportsImageWithMaxCount 4 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebPageWithMaxCount + 1 + NSExtensionActivationSupportsWebURLWithMaxCount + 1 NSExtensionMainStoryboard diff --git a/IceCubesShareExtension/ShareViewController.swift b/IceCubesShareExtension/ShareViewController.swift index 9528f41e..6abda4e3 100644 --- a/IceCubesShareExtension/ShareViewController.swift +++ b/IceCubesShareExtension/ShareViewController.swift @@ -1,18 +1,18 @@ +import Account +import AppAccount +import DesignSystem +import Env +import Network +import Status import SwiftUI import UIKit -import Status -import DesignSystem -import Account -import Network -import Env -import AppAccount class ShareViewController: UIViewController { @IBOutlet var container: UIView! - + override func viewDidLoad() { super.viewDidLoad() - + let appAccountsManager = AppAccountsManager.shared let client = appAccountsManager.currentClient let account = CurrentAccount.shared @@ -22,7 +22,7 @@ class ShareViewController: UIViewController { let colorScheme = traitCollection.userInterfaceStyle let theme = Theme.shared theme.setColor(withName: colorScheme == .dark ? .iceCubeDark : .iceCubeLight) - + if let item = extensionContext?.inputItems.first as? NSExtensionItem { if let attachments = item.attachments { let view = StatusEditorView(mode: .shareExtension(items: attachments)) @@ -35,24 +35,24 @@ class ShareViewController: UIViewController { .tint(theme.tintColor) .preferredColorScheme(colorScheme == .light ? .light : .dark) let childView = UIHostingController(rootView: view) - self.addChild(childView) - childView.view.frame = self.container.bounds - self.container.addSubview(childView.view) + addChild(childView) + childView.view.frame = container.bounds + container.addSubview(childView.view) childView.didMove(toParent: self) } } - + NotificationCenter.default.addObserver(forName: NotificationsName.shareSheetClose, object: nil, queue: nil) { _ in - self.close() + self.close() } } - + func close() { extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } diff --git a/Packages/Account/Package.swift b/Packages/Account/Package.swift index d24d3893..81073852 100644 --- a/Packages/Account/Package.swift +++ b/Packages/Account/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "Account", - targets: ["Account"]), + targets: ["Account"] + ), ], dependencies: [ .package(name: "Network", path: "../Network"), @@ -25,9 +26,11 @@ let package = Package( .product(name: "Network", package: "Network"), .product(name: "Models", package: "Models"), .product(name: "Status", package: "Status"), - ]), + ] + ), .testTarget( name: "AccountTests", - dependencies: ["Account"]), + dependencies: ["Account"] + ), ] ) diff --git a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift index 93b12005..c1f94a60 100644 --- a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift +++ b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift @@ -1,23 +1,23 @@ -import SwiftUI -import Models import DesignSystem -import Env -import Shimmer -import NukeUI import EmojiText +import Env +import Models +import NukeUI +import Shimmer +import SwiftUI struct AccountDetailHeaderView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var quickLook: QuickLook @EnvironmentObject private var routeurPath: RouterPath @Environment(\.redactionReasons) private var reasons - + @ObservedObject var viewModel: AccountDetailViewModel let account: Account let scrollViewProxy: ScrollViewProxy? - + @Binding var scrollOffset: CGFloat - + private var bannerHeight: CGFloat { 200 + (scrollOffset > 0 ? scrollOffset * 2 : 0) } @@ -28,9 +28,9 @@ struct AccountDetailHeaderView: View { accountInfoView } } - + private var headerImageView: some View { - GeometryReader { proxy in + GeometryReader { _ in ZStack(alignment: .bottomTrailing) { if reasons.contains(.placeholder) { Rectangle() @@ -53,7 +53,7 @@ struct AccountDetailHeaderView: View { } .frame(height: bannerHeight) } - + if viewModel.relationship?.followedBy == true { Text("Follows You") .font(.footnote) @@ -75,15 +75,15 @@ struct AccountDetailHeaderView: View { } } } - + private var accountAvatarView: some View { HStack { AvatarView(url: account.avatar, size: .account) - .onTapGesture { - Task { - await quickLook.prepareFor(urls: [account.avatar], selectedURL: account.avatar) + .onTapGesture { + Task { + await quickLook.prepareFor(urls: [account.avatar], selectedURL: account.avatar) + } } - } Spacer() Group { Button { @@ -102,17 +102,17 @@ struct AccountDetailHeaderView: View { }.offset(y: 20) } } - + private var accountInfoView: some View { Group { accountAvatarView HStack { VStack(alignment: .leading, spacing: 0) { EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis) - .font(.headline) + .font(.headline) Text("@\(account.acct)") - .font(.callout) - .foregroundColor(.gray) + .font(.callout) + .foregroundColor(.gray) } Spacer() if let relationship = viewModel.relationship, !viewModel.isCurrentUser { @@ -133,7 +133,7 @@ struct AccountDetailHeaderView: View { .padding(.horizontal, .layoutPadding) .offset(y: -40) } - + private func makeCustomInfoLabel(title: String, count: Int) -> some View { VStack { Text("\(count)") diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index 6fba11bd..69802d71 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -1,13 +1,13 @@ -import SwiftUI +import DesignSystem +import EmojiText +import Env import Models import Network -import Status import Shimmer -import DesignSystem -import Env -import EmojiText +import Status +import SwiftUI -public struct AccountDetailView: View { +public struct AccountDetailView: View { @Environment(\.redactionReasons) private var reasons @EnvironmentObject private var watcher: StreamWatcher @EnvironmentObject private var currentAccount: CurrentAccount @@ -15,7 +15,7 @@ public struct AccountDetailView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var client: Client @EnvironmentObject private var routeurPath: RouterPath - + @StateObject private var viewModel: AccountDetailViewModel @State private var scrollOffset: CGFloat = 0 @State private var isFieldsSheetDisplayed: Bool = false @@ -23,17 +23,17 @@ public struct AccountDetailView: View { @State private var isCreateListAlertPresented: Bool = false @State private var createListTitle: String = "" @State private var isEditingAccount: Bool = false - + /// When coming from a URL like a mention tap in a status. public init(accountId: String) { _viewModel = StateObject(wrappedValue: .init(accountId: accountId)) } - + /// When the account is already fetched by the parent caller. public init(account: Account) { _viewModel = StateObject(wrappedValue: .init(account: account)) } - + public var body: some View { ScrollViewReader { proxy in ScrollViewOffsetReader { offset in @@ -58,7 +58,7 @@ public struct AccountDetailView: View { .offset(y: -20) } .id("status") - + switch viewModel.tabState { case .statuses: if viewModel.selectedTab == .statuses { @@ -94,9 +94,10 @@ public struct AccountDetailView: View { await viewModel.fetchStatuses() } } - .onChange(of: watcher.latestEvent?.id) { id in + .onChange(of: watcher.latestEvent?.id) { _ in if let latestEvent = watcher.latestEvent, - viewModel.accountId == currentAccount.account?.id { + viewModel.accountId == currentAccount.account?.id + { viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount) } } @@ -117,7 +118,7 @@ public struct AccountDetailView: View { toolbarContent } } - + @ViewBuilder private func makeHeaderView(proxy: ScrollViewProxy?) -> some View { switch viewModel.accountState { @@ -137,7 +138,7 @@ public struct AccountDetailView: View { Text("Error: \(error.localizedDescription)") } } - + @ViewBuilder private var featuredTagsView: some View { if !viewModel.featuredTags.isEmpty || !viewModel.fields.isEmpty { @@ -178,7 +179,7 @@ public struct AccountDetailView: View { } } } - + @ViewBuilder private var familliarFollowers: some View { if !viewModel.familliarFollowers.isEmpty { @@ -203,7 +204,7 @@ public struct AccountDetailView: View { .padding(.bottom, 12) } } - + private var fieldSheetView: some View { NavigationStack { List { @@ -244,7 +245,7 @@ public struct AccountDetailView: View { } } } - + private var tagsListView: some View { Group { ForEach(currentAccount.tags) { tag in @@ -260,7 +261,7 @@ public struct AccountDetailView: View { await currentAccount.fetchFollowedTags() } } - + private var listsListView: some View { Group { ForEach(currentAccount.lists) { list in @@ -309,7 +310,7 @@ public struct AccountDetailView: View { Text("Enter the name for your list") } } - + @ViewBuilder private var pinnedPostsView: some View { if !viewModel.pinned.isEmpty { @@ -327,7 +328,7 @@ public struct AccountDetailView: View { } } } - + @ToolbarContentBuilder private var toolbarContent: some ToolbarContent { ToolbarItem(placement: .principal) { @@ -341,7 +342,7 @@ public struct AccountDetailView: View { } } } - + ToolbarItem(placement: .navigationBarTrailing) { Menu { if let account = viewModel.account { @@ -360,7 +361,7 @@ public struct AccountDetailView: View { } Divider() } - + if viewModel.relationship?.following == true { Button { routeurPath.presentedSheet = .listAddAccount(account: account) @@ -368,13 +369,13 @@ public struct AccountDetailView: View { Label("Add/Remove from lists", systemImage: "list.bullet") } } - + if let url = account.url { ShareLink(item: url) } - + Divider() - + if isCurrentUser { Button { isEditingAccount = true @@ -396,4 +397,3 @@ struct AccountDetailView_Previews: PreviewProvider { AccountDetailView(account: .placeholder()) } } - diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index 9ebd3b47..b6fc370e 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -1,30 +1,30 @@ -import SwiftUI -import Network -import Models -import Status import Env +import Models +import Network +import Status +import SwiftUI @MainActor class AccountDetailViewModel: ObservableObject, StatusesFetcher { let accountId: String var client: Client? var isCurrentUser: Bool = false - + enum AccountState { case loading, data(account: Account), error(error: Error) } - + enum Tab: Int { case statuses, favourites, bookmarks, followedTags, postsAndReplies, media, lists - + static var currentAccountTabs: [Tab] { [.statuses, .favourites, .bookmarks, .followedTags, .lists] } - + static var accountTabs: [Tab] { [.statuses, .postsAndReplies, .media] } - + var iconName: String { switch self { case .statuses: return "bubble.right" @@ -37,13 +37,13 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } } } - + enum TabState { case followedTags case statuses(statusesState: StatusesState) case lists } - + @Published var accountState: AccountState = .loading @Published var tabState: TabState = .statuses(statusesState: .loading) { didSet { @@ -57,8 +57,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } } } + @Published var statusesState: StatusesState = .loading - + @Published var relationship: Relationshionship? @Published var pinned: [Status] = [] @Published var favourites: [Status] = [] @@ -81,45 +82,45 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } } } - + private(set) var account: Account? private var tabTask: Task? - + private(set) var statuses: [Status] = [] - + /// When coming from a URL like a mention tap in a status. init(accountId: String) { self.accountId = accountId - self.isCurrentUser = false + isCurrentUser = false } - + /// When the account is already fetched by the parent caller. init(account: Account) { - self.accountId = account.id + accountId = account.id self.account = account - self.accountState = .data(account: account) + accountState = .data(account: account) } - + struct AccountData { let account: Account let featuredTags: [FeaturedTag] let relationships: [Relationshionship] let familliarFollowers: [FamilliarAccounts] } - + func fetchAccount() async { guard let client else { return } do { let data = try await fetchAccountData(accountId: accountId, client: client) accountState = .data(account: data.account) - + account = data.account fields = data.account.fields featuredTags = data.featuredTags featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt } relationship = data.relationships.first familliarFollowers = data.familliarFollowers.first?.accounts ?? [] - + } catch { if let account { accountState = .data(account: account) @@ -128,7 +129,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } } } - + 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)) @@ -145,26 +146,26 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { relationships: [], familliarFollowers: []) } - + func fetchStatuses() async { guard let client else { return } do { tabState = .statuses(statusesState: .loading) statuses = - try await client.get(endpoint: Accounts.statuses(id: accountId, - sinceId: nil, - tag: nil, - onlyMedia: selectedTab == .media ? true : nil, - excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil, - pinned: nil)) - if selectedTab == .statuses { - pinned = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil, tag: nil, - onlyMedia: nil, - excludeReplies: nil, - pinned: true)) + onlyMedia: selectedTab == .media ? true : nil, + excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil, + pinned: nil)) + if selectedTab == .statuses { + pinned = + try await client.get(endpoint: Accounts.statuses(id: accountId, + sinceId: nil, + tag: nil, + onlyMedia: nil, + excludeReplies: nil, + pinned: true)) } if isCurrentUser { (favourites, favouritesNextPage) = try await client.getWithLink(endpoint: Accounts.favourites(sinceId: nil)) @@ -175,7 +176,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { tabState = .statuses(statusesState: .error(error: error)) } } - + func fetchNextPage() async { guard let client else { return } do { @@ -184,12 +185,12 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { guard let lastId = statuses.last?.id else { return } tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .loadingNextPage)) let newStatuses: [Status] = - try await client.get(endpoint: Accounts.statuses(id: accountId, - sinceId: lastId, - tag: nil, - onlyMedia: selectedTab == .media ? true : nil, - excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil, - pinned: nil)) + try await client.get(endpoint: Accounts.statuses(id: accountId, + sinceId: lastId, + tag: nil, + onlyMedia: selectedTab == .media ? true : nil, + excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil, + pinned: nil)) statuses.append(contentsOf: newStatuses) tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)) @@ -212,7 +213,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { tabState = .statuses(statusesState: .error(error: error)) } } - + private func reloadTabState() { switch selectedTab { case .statuses, .postsAndReplies, .media: @@ -229,7 +230,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { tabState = .lists } } - + func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) { if let event = event as? StreamEventUpdate { if event.status.account.id == currentAccount.account?.id { diff --git a/Packages/Account/Sources/Account/AccountsLIst/AccountsListRow.swift b/Packages/Account/Sources/Account/AccountsLIst/AccountsListRow.swift index de0a3d65..fac82d59 100644 --- a/Packages/Account/Sources/Account/AccountsLIst/AccountsListRow.swift +++ b/Packages/Account/Sources/Account/AccountsLIst/AccountsListRow.swift @@ -1,17 +1,17 @@ -import SwiftUI +import DesignSystem +import EmojiText +import Env import Models import Network -import DesignSystem -import Env -import EmojiText +import SwiftUI @MainActor public class AccountsListRowViewModel: ObservableObject { var client: Client? - + @Published var account: Account @Published var relationShip: Relationshionship - + public init(account: Account, relationShip: Relationshionship) { self.account = account self.relationShip = relationShip @@ -22,13 +22,13 @@ public struct AccountsListRow: View { @EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var routeurPath: RouterPath @EnvironmentObject private var client: Client - + @StateObject var viewModel: AccountsListRowViewModel - + public init(viewModel: AccountsListRowViewModel) { _viewModel = StateObject(wrappedValue: viewModel) } - + public var body: some View { HStack(alignment: .top) { AvatarView(url: viewModel.account.avatar, size: .status) diff --git a/Packages/Account/Sources/Account/AccountsLIst/AccountsListView.swift b/Packages/Account/Sources/Account/AccountsLIst/AccountsListView.swift index c8d02ea6..91298757 100644 --- a/Packages/Account/Sources/Account/AccountsLIst/AccountsListView.swift +++ b/Packages/Account/Sources/Account/AccountsLIst/AccountsListView.swift @@ -1,25 +1,25 @@ -import SwiftUI -import Network -import Models -import Env -import Shimmer import DesignSystem +import Env +import Models +import Network +import Shimmer +import SwiftUI public struct AccountsListView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var client: Client @StateObject private var viewModel: AccountsListViewModel @State private var didAppear: Bool = false - + public init(mode: AccountsListMode) { _viewModel = StateObject(wrappedValue: .init(mode: mode)) } - + public var body: some View { List { switch viewModel.state { case .loading: - ForEach(Account.placeholders()) { account in + ForEach(Account.placeholders()) { _ in AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder())) .redacted(reason: .placeholder) .shimmering() @@ -30,10 +30,10 @@ public struct AccountsListView: View { if let relationship = relationships.first(where: { $0.id == account.id }) { AccountsListRow(viewModel: .init(account: account, relationShip: relationship)) - .listRowBackground(theme.primaryBackgroundColor) + .listRowBackground(theme.primaryBackgroundColor) } } - + switch nextPageState { case .hasNextPage: loadingRow @@ -43,14 +43,14 @@ public struct AccountsListView: View { await viewModel.fetchNextPage() } } - + case .loadingNextPage: loadingRow .listRowBackground(theme.primaryBackgroundColor) case .none: EmptyView() } - + case let .error(error): Text(error.localizedDescription) .listRowBackground(theme.primaryBackgroundColor) @@ -63,12 +63,12 @@ public struct AccountsListView: View { .navigationBarTitleDisplayMode(.inline) .task { viewModel.client = client - guard !didAppear else { return} + guard !didAppear else { return } didAppear = true await viewModel.fetch() } } - + private var loadingRow: some View { HStack { Spacer() diff --git a/Packages/Account/Sources/Account/AccountsLIst/AccountsListViewModel.swift b/Packages/Account/Sources/Account/AccountsLIst/AccountsListViewModel.swift index 1926fd22..2b753b7d 100644 --- a/Packages/Account/Sources/Account/AccountsLIst/AccountsListViewModel.swift +++ b/Packages/Account/Sources/Account/AccountsLIst/AccountsListViewModel.swift @@ -1,11 +1,11 @@ -import SwiftUI import Models import Network +import SwiftUI public enum AccountsListMode { case following(accountId: String), followers(accountId: String) case favouritedBy(statusId: String), rebloggedBy(statusId: String) - + var title: String { switch self { case .following: @@ -23,31 +23,32 @@ public enum AccountsListMode { @MainActor class AccountsListViewModel: ObservableObject { var client: Client? - + let mode: AccountsListMode - + public enum State { public enum PagingState { case hasNextPage, loadingNextPage, none } + case loading case display(accounts: [Account], relationships: [Relationshionship], nextPageState: PagingState) case error(error: Error) } - + private var accounts: [Account] = [] private var relationships: [Relationshionship] = [] - + @Published var state = State.loading - + private var nextPageId: String? - + init(mode: AccountsListMode) { self.mode = mode } - + func fetch() async { guard let client else { return } do { @@ -69,13 +70,13 @@ class AccountsListViewModel: ObservableObject { } nextPageId = link?.maxId relationships = try await client.get(endpoint: - Accounts.relationships(ids: accounts.map{ $0.id })) + Accounts.relationships(ids: accounts.map { $0.id })) state = .display(accounts: accounts, relationships: relationships, nextPageState: link?.maxId != nil ? .hasNextPage : .none) - } catch { } + } catch {} } - + func fetchNextPage() async { guard let client, let nextPageId else { return } do { @@ -93,13 +94,13 @@ class AccountsListViewModel: ObservableObject { (newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId, maxId: nextPageId)) case let .favouritedBy(statusId): - (newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favouritedBy(id: statusId, - maxId: nextPageId)) + (newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favouritedBy(id: statusId, + maxId: nextPageId)) } accounts.append(contentsOf: newAccounts) let newRelationships: [Relationshionship] = - try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map{ $0.id })) - + try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map { $0.id })) + relationships.append(contentsOf: newRelationships) self.nextPageId = link?.maxId state = .display(accounts: accounts, diff --git a/Packages/Account/Sources/Account/Edit/EditAccountView.swift b/Packages/Account/Sources/Account/Edit/EditAccountView.swift index 9362597e..e2450bf1 100644 --- a/Packages/Account/Sources/Account/Edit/EditAccountView.swift +++ b/Packages/Account/Sources/Account/Edit/EditAccountView.swift @@ -1,15 +1,15 @@ -import SwiftUI +import DesignSystem import Models import Network -import DesignSystem +import SwiftUI struct EditAccountView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var client: Client @EnvironmentObject private var theme: Theme - + @StateObject private var viewModel = EditAccountViewModel() - + public var body: some View { NavigationStack { Form { @@ -31,15 +31,15 @@ struct EditAccountView: View { .alert("Error while saving your profile", isPresented: $viewModel.saveError, actions: { - Button("Ok", action: { }) - }, message: { Text("Error while saving your profile, please try again.") }) + Button("Ok", action: {}) + }, message: { Text("Error while saving your profile, please try again.") }) .task { viewModel.client = client await viewModel.fetchAccount() } } } - + private var loadingSection: some View { Section { HStack { @@ -50,7 +50,7 @@ struct EditAccountView: View { } .listRowBackground(theme.primaryBackgroundColor) } - + @ViewBuilder private var aboutSections: some View { Section("Display Name") { @@ -63,7 +63,7 @@ struct EditAccountView: View { } .listRowBackground(theme.primaryBackgroundColor) } - + private var postSettingsSection: some View { Section("Post settings") { Picker(selection: $viewModel.postPrivacy) { @@ -80,7 +80,7 @@ struct EditAccountView: View { } .listRowBackground(theme.primaryBackgroundColor) } - + private var accountSection: some View { Section("Account settings") { Toggle(isOn: $viewModel.isLocked) { @@ -95,7 +95,7 @@ struct EditAccountView: View { } .listRowBackground(theme.primaryBackgroundColor) } - + @ToolbarContentBuilder private var toolbarContent: some ToolbarContent { ToolbarItem(placement: .navigationBarLeading) { @@ -103,7 +103,7 @@ struct EditAccountView: View { dismiss() } } - + ToolbarItem(placement: .navigationBarTrailing) { Button { Task { diff --git a/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift b/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift index 972e719b..ca99756e 100644 --- a/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift +++ b/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift @@ -1,11 +1,11 @@ -import SwiftUI import Models import Network +import SwiftUI @MainActor class EditAccountViewModel: ObservableObject { public var client: Client? - + @Published var displayName: String = "" @Published var note: String = "" @Published var postPrivacy = Models.Visibility.pub @@ -13,13 +13,13 @@ class EditAccountViewModel: ObservableObject { @Published var isBot: Bool = false @Published var isLocked: Bool = false @Published var isDiscoverable: Bool = false - + @Published var isLoading: Bool = true @Published var isSaving: Bool = false @Published var saveError: Bool = false - - init() { } - + + init() {} + func fetchAccount() async { guard let client else { return } do { @@ -34,20 +34,20 @@ class EditAccountViewModel: ObservableObject { withAnimation { isLoading = false } - } catch { } + } catch {} } - + func save() async { isSaving = true do { let response = - try await client?.patch(endpoint: Accounts.updateCredentials(displayName: displayName, - note: note, - privacy: postPrivacy, - isSensitive: isSensitive, - isBot: isBot, - isLocked: isLocked, - isDiscoverable: isDiscoverable)) + try await client?.patch(endpoint: Accounts.updateCredentials(displayName: displayName, + note: note, + privacy: postPrivacy, + isSensitive: isSensitive, + isBot: isBot, + isLocked: isLocked, + isDiscoverable: isDiscoverable)) if response?.statusCode != 200 { saveError = true } @@ -57,5 +57,4 @@ class EditAccountViewModel: ObservableObject { saveError = true } } - } diff --git a/Packages/Account/Sources/Account/Follow/FollowButton.swift b/Packages/Account/Sources/Account/Follow/FollowButton.swift index cdae31b3..ffca4600 100644 --- a/Packages/Account/Sources/Account/Follow/FollowButton.swift +++ b/Packages/Account/Sources/Account/Follow/FollowButton.swift @@ -1,23 +1,23 @@ import Foundation -import SwiftUI import Models import Network +import SwiftUI @MainActor public class FollowButtonViewModel: ObservableObject { var client: Client? - + public let accountId: String public let shouldDisplayNotify: Bool - @Published private(set) public var relationship: Relationshionship - @Published private(set) public var isUpdating: Bool = false - + @Published public private(set) var relationship: Relationshionship + @Published public private(set) var isUpdating: Bool = false + public init(accountId: String, relationship: Relationshionship, shouldDisplayNotify: Bool) { self.accountId = accountId self.relationship = relationship self.shouldDisplayNotify = shouldDisplayNotify } - + func follow() async { guard let client else { return } isUpdating = true @@ -28,7 +28,7 @@ public class FollowButtonViewModel: ObservableObject { } isUpdating = false } - + func unfollow() async { guard let client else { return } isUpdating = true @@ -39,7 +39,7 @@ public class FollowButtonViewModel: ObservableObject { } isUpdating = false } - + func toggleNotify() async { guard let client else { return } do { @@ -53,11 +53,11 @@ public class FollowButtonViewModel: ObservableObject { public struct FollowButton: View { @EnvironmentObject private var client: Client @StateObject private var viewModel: FollowButtonViewModel - + public init(viewModel: FollowButtonViewModel) { _viewModel = StateObject(wrappedValue: viewModel) } - + public var body: some View { HStack { Button { diff --git a/Packages/Account/Tests/AccountTests/AccountTests.swift b/Packages/Account/Tests/AccountTests/AccountTests.swift index 230dbfb7..f16a89fe 100644 --- a/Packages/Account/Tests/AccountTests/AccountTests.swift +++ b/Packages/Account/Tests/AccountTests/AccountTests.swift @@ -1,11 +1,11 @@ -import XCTest @testable import Account +import XCTest final class AccountTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(Account().text, "Hello, World!") - } + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Account().text, "Hello, World!") + } } diff --git a/Packages/AppAccount/Package.swift b/Packages/AppAccount/Package.swift index 86776f8a..e6e5399b 100644 --- a/Packages/AppAccount/Package.swift +++ b/Packages/AppAccount/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "AppAccount", - targets: ["AppAccount"]), + targets: ["AppAccount"] + ), ], dependencies: [ .package(name: "Network", path: "../Network"), @@ -27,6 +28,7 @@ let package = Package( .product(name: "Models", package: "Models"), .product(name: "Env", package: "Env"), .product(name: "DesignSystem", package: "DesignSystem"), - ]) + ] + ), ] ) diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccount.swift b/Packages/AppAccount/Sources/AppAccount/AppAccount.swift index 0c3db656..10016514 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccount.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccount.swift @@ -1,17 +1,17 @@ -import SwiftUI -import Network +import CryptoKit import KeychainSwift import Models -import CryptoKit +import Network +import SwiftUI public struct AppAccount: Codable, Identifiable { public let server: String public let oauthToken: OauthToken? - + public var id: String { key } - + private static var keychain: KeychainSwift { let keychain = KeychainSwift() #if !DEBUG @@ -19,7 +19,7 @@ public struct AppAccount: Codable, Identifiable { #endif return keychain } - + public var key: String { if let oauthToken { return "\(server):\(oauthToken.createdAt)" @@ -27,22 +27,22 @@ public struct AppAccount: Codable, Identifiable { return "\(server):anonymous:\(Date().timeIntervalSince1970)" } } - + public init(server: String, oauthToken: OauthToken? = nil) { self.server = server self.oauthToken = oauthToken } - + public func save() throws { let encoder = JSONEncoder() let data = try encoder.encode(self) Self.keychain.set(data, forKey: key) } - + public func delete() { Self.keychain.delete(key) } - + public static func retrieveAll() -> [AppAccount] { migrateLegacyAccounts() let keychain = Self.keychain @@ -58,7 +58,7 @@ public struct AppAccount: Codable, Identifiable { } return accounts } - + public static func migrateLegacyAccounts() { let keychain = KeychainSwift() let decoder = JSONDecoder() @@ -71,7 +71,7 @@ public struct AppAccount: Codable, Identifiable { } } } - + public static func deleteAll() { let keychain = Self.keychain let keys = keychain.allKeys diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift index 84f036d9..ff6585bf 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift @@ -1,17 +1,17 @@ -import SwiftUI import DesignSystem -import Env import EmojiText +import Env +import SwiftUI public struct AppAccountView: View { @EnvironmentObject private var routeurPath: RouterPath @EnvironmentObject var appAccounts: AppAccountsManager @StateObject var viewModel: AppAccountViewModel - + public init(viewModel: AppAccountViewModel) { _viewModel = .init(wrappedValue: viewModel) } - + public var body: some View { HStack { if let account = viewModel.account { @@ -43,7 +43,8 @@ public struct AppAccountView: View { } .onTapGesture { if appAccounts.currentAccount.id == viewModel.appAccount.id, - let account = viewModel.account { + let account = viewModel.account + { routeurPath.navigate(to: .accountDetailWithAccount(account: account)) } else { appAccounts.currentAccount = viewModel.appAccount diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift index b1197f5c..373fb9f3 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift @@ -1,23 +1,23 @@ -import SwiftUI import Models import Network +import SwiftUI @MainActor public class AppAccountViewModel: ObservableObject { let appAccount: AppAccount let client: Client - + @Published var account: Account? - + var acct: String { "@\(account?.acct ?? "...")@\(appAccount.server)" } - + public init(appAccount: AppAccount) { self.appAccount = appAccount - self.client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken) + client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken) } - + func fetchAccount() async { do { account = try await client.get(endpoint: Accounts.verifyCredentials) diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift index 313afb80..a5c9afa9 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift @@ -1,13 +1,13 @@ -import SwiftUI -import Network import Env import Models +import Network +import SwiftUI @MainActor public class AppAccountsManager: ObservableObject { @AppStorage("latestCurrentAccountKey", store: UserPreferences.sharedDefault) - static public var latestCurrentAccountKey: String = "" - + public static var latestCurrentAccountKey: String = "" + @Published public var currentAccount: AppAccount { didSet { Self.latestCurrentAccountKey = currentAccount.id @@ -15,16 +15,17 @@ public class AppAccountsManager: ObservableObject { oauthToken: currentAccount.oauthToken) } } + @Published public var availableAccounts: [AppAccount] @Published public var currentClient: Client - + public var pushAccounts: [PushNotificationsService.PushAccounts] { - availableAccounts.filter{ $0.oauthToken != nil} - .map{ .init(server: $0.server, token: $0.oauthToken!) } + availableAccounts.filter { $0.oauthToken != nil } + .map { .init(server: $0.server, token: $0.oauthToken!) } } - + public static var shared = AppAccountsManager() - + internal init() { var defaultAccount = AppAccount(server: AppInfo.defaultServer, oauthToken: nil) let keychainAccounts = AppAccount.retrieveAll() @@ -37,15 +38,15 @@ public class AppAccountsManager: ObservableObject { currentAccount = defaultAccount currentClient = .init(server: defaultAccount.server, oauthToken: defaultAccount.oauthToken) } - + public func add(account: AppAccount) { do { try account.save() availableAccounts.append(account) currentAccount = account - } catch { } + } catch {} } - + public func delete(account: AppAccount) { availableAccounts.removeAll(where: { $0.id == account.id }) account.delete() diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift index 9980c879..d0f639c4 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift @@ -1,26 +1,27 @@ -import SwiftUI -import Env import DesignSystem +import Env +import SwiftUI public struct AppAccountsSelectorView: View { @EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var appAccounts: AppAccountsManager - + @ObservedObject var routeurPath: RouterPath - + @State private var accountsViewModel: [AppAccountViewModel] = [] - + private let accountCreationEnabled: Bool private let avatarSize: AvatarView.Size - + public init(routeurPath: RouterPath, accountCreationEnabled: Bool = true, - avatarSize: AvatarView.Size = .badge) { + avatarSize: AvatarView.Size = .badge) + { self.routeurPath = routeurPath self.accountCreationEnabled = accountCreationEnabled self.avatarSize = avatarSize } - + public var body: some View { Group { if UIDevice.current.userInterfaceIdiom == .pad { @@ -43,7 +44,7 @@ public struct AppAccountsSelectorView: View { refreshAccounts() } } - + @ViewBuilder private var labelView: some View { if let avatar = currentAccount.account?.avatar { @@ -52,14 +53,15 @@ public struct AppAccountsSelectorView: View { EmptyView() } } - + @ViewBuilder private var menuView: some View { ForEach(accountsViewModel, id: \.appAccount.id) { viewModel in Section(viewModel.acct) { Button { if let account = currentAccount.account, - viewModel.account?.id == account.id { + viewModel.account?.id == account.id + { routeurPath.navigate(to: .accountDetailWithAccount(account: account)) } else { appAccounts.currentAccount = viewModel.appAccount @@ -83,7 +85,7 @@ public struct AppAccountsSelectorView: View { } } } - + private func refreshAccounts() { if accountsViewModel.isEmpty || appAccounts.availableAccounts.count != accountsViewModel.count { accountsViewModel = [] @@ -98,5 +100,4 @@ public struct AppAccountsSelectorView: View { } } } - } diff --git a/Packages/Conversations/Package.swift b/Packages/Conversations/Package.swift index 7f34838b..995b824f 100644 --- a/Packages/Conversations/Package.swift +++ b/Packages/Conversations/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "Conversations", - targets: ["Conversations"]), + targets: ["Conversations"] + ), ], dependencies: [ .package(name: "Models", path: "../Models"), @@ -27,7 +28,7 @@ let package = Package( .product(name: "Network", package: "Network"), .product(name: "Env", package: "Env"), .product(name: "DesignSystem", package: "DesignSystem"), - ]), + ] + ), ] ) - diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift index d4c95abf..7d545321 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift @@ -1,25 +1,25 @@ -import SwiftUI -import Models import Accounts import DesignSystem import Env +import Models import Network +import SwiftUI struct ConversationsListRow: View { @EnvironmentObject private var client: Client @EnvironmentObject private var routerPath: RouterPath @EnvironmentObject private var theme: Theme - + let conversation: Conversation @ObservedObject var viewModel: ConversationsListViewModel - + var body: some View { VStack(alignment: .leading) { HStack(alignment: .top, spacing: 8) { AvatarView(url: conversation.accounts.first!.avatar) VStack(alignment: .leading, spacing: 4) { HStack { - Text(conversation.accounts.map{ $0.safeDisplayName }.joined(separator: ", ")) + Text(conversation.accounts.map { $0.safeDisplayName }.joined(separator: ", ")) .font(.headline) .foregroundColor(theme.labelColor) .multilineTextAlignment(.leading) @@ -52,7 +52,7 @@ struct ConversationsListRow: View { contextMenu } } - + private var actionsView: some View { HStack(spacing: 12) { Button { @@ -71,7 +71,7 @@ struct ConversationsListRow: View { .padding(.leading, 48) .foregroundColor(.gray) } - + @ViewBuilder private var contextMenu: some View { Button { @@ -81,7 +81,7 @@ struct ConversationsListRow: View { } label: { Label("Mark as read", systemImage: "eye") } - + Button(role: .destructive) { Task { await viewModel.delete(conversation: conversation) diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift index 60a88050..59a40827 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift @@ -1,27 +1,27 @@ -import SwiftUI -import Network -import Models import DesignSystem -import Shimmer import Env +import Models +import Network +import Shimmer +import SwiftUI public struct ConversationsListView: View { @EnvironmentObject private var routeurPath: RouterPath @EnvironmentObject private var watcher: StreamWatcher @EnvironmentObject private var client: Client @EnvironmentObject private var theme: Theme - + @StateObject private var viewModel = ConversationsListViewModel() - - public init() { } - + + public init() {} + private var conversations: [Conversation] { if viewModel.isLoadingFirstPage { return Conversation.placeholders() } return viewModel.conversations } - + public var body: some View { ScrollView { LazyVStack { @@ -61,7 +61,7 @@ public struct ConversationsListView: View { .toolbar { StatusEditorToolbarItem(visibility: .direct) } - .onChange(of: watcher.latestEvent?.id) { id in + .onChange(of: watcher.latestEvent?.id) { _ in if let latestEvent = watcher.latestEvent { viewModel.handleEvent(event: latestEvent) } diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift index 2727696d..3799efaa 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift @@ -5,13 +5,13 @@ import SwiftUI @MainActor class ConversationsListViewModel: ObservableObject { var client: Client? - + @Published var isLoadingFirstPage: Bool = true @Published var conversations: [Conversation] = [] @Published var isError: Bool = false - - public init() { } - + + public init() {} + func fetchConversations() async { guard let client else { return } if conversations.isEmpty { @@ -25,18 +25,18 @@ class ConversationsListViewModel: ObservableObject { isLoadingFirstPage = false } } - + func markAsRead(conversation: Conversation) async { guard let client else { return } _ = try? await client.post(endpoint: Conversations.read(id: conversation.id)) } - + func delete(conversation: Conversation) async { guard let client else { return } _ = try? await client.delete(endpoint: Conversations.delete(id: conversation.id)) await fetchConversations() } - + func handleEvent(event: any StreamEvent) { if let event = event as? StreamEventConversation { if let index = conversations.firstIndex(where: { $0.id == event.conversation.id }) { diff --git a/Packages/DesignSystem/Package.swift b/Packages/DesignSystem/Package.swift index a27deb3e..0ba15ca1 100644 --- a/Packages/DesignSystem/Package.swift +++ b/Packages/DesignSystem/Package.swift @@ -11,14 +11,15 @@ let package = Package( products: [ .library( name: "DesignSystem", - targets: ["DesignSystem"]), + targets: ["DesignSystem"] + ), ], dependencies: [ .package(name: "Models", path: "../Models"), .package(name: "Env", path: "../Env"), .package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0"), .package(url: "https://github.com/kean/Nuke", from: "11.5.0"), - .package(url: "https://github.com/divadretlaw/EmojiText", from: "1.1.0") + .package(url: "https://github.com/divadretlaw/EmojiText", from: "1.1.0"), ], targets: [ .target( @@ -29,8 +30,8 @@ let package = Package( .product(name: "Shimmer", package: "SwiftUI-Shimmer"), .product(name: "NukeUI", package: "Nuke"), .product(name: "Nuke", package: "Nuke"), - .product(name: "EmojiText", package: "EmojiText") - ]), + .product(name: "EmojiText", package: "EmojiText"), + ] + ), ] ) - diff --git a/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift b/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift index 8c24ab8b..ec83d09a 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift @@ -1,22 +1,22 @@ import Foundation -import SwiftUI -import NukeUI import Models +import NukeUI +import SwiftUI -extension Account { +public extension Account { private struct Part: Identifiable { let id = UUID().uuidString let value: Substring } - - public var safeDisplayName: String { + + var safeDisplayName: String { if displayName.isEmpty { return username } return displayName } - - public var displayNameWithoutEmojis: String { + + 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 09e2076e..3b8463b8 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/ColorSet.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/ColorSet.swift @@ -24,68 +24,65 @@ public enum ColorSetName: String { public struct IceCubeDark: ColorSet { public var name: ColorSetName = .iceCubeDark public var scheme: ColorScheme = .dark - public var tintColor: Color = Color(red: 187/255, green: 59/255, blue: 226/255) - public var primaryBackgroundColor: Color = Color(red: 16/255, green: 21/255, blue: 35/255) - public var secondaryBackgroundColor: Color = Color(red: 30/255, green: 35/255, blue: 62/255) + public var tintColor: Color = .init(red: 187 / 255, green: 59 / 255, blue: 226 / 255) + public var primaryBackgroundColor: Color = .init(red: 16 / 255, green: 21 / 255, blue: 35 / 255) + public var secondaryBackgroundColor: Color = .init(red: 30 / 255, green: 35 / 255, blue: 62 / 255) public var labelColor: Color = .white - + public init() {} } public struct IceCubeLight: ColorSet { public var name: ColorSetName = .iceCubeLight public var scheme: ColorScheme = .light - public var tintColor: Color = Color(red: 187/255, green: 59/255, blue: 226/255) + public var tintColor: Color = .init(red: 187 / 255, green: 59 / 255, blue: 226 / 255) public var primaryBackgroundColor: Color = .white - public var secondaryBackgroundColor: Color = Color(hex:0xF0F1F2) + public var secondaryBackgroundColor: Color = .init(hex: 0xF0F1F2) public var labelColor: Color = .black - + public init() {} } public struct DesertDark: ColorSet { public var name: ColorSetName = .desertDark public var scheme: ColorScheme = .dark - public var tintColor: Color = Color(hex: 0xdf915e) - public var primaryBackgroundColor: Color = Color(hex: 0x433744) - public var secondaryBackgroundColor: Color = Color(hex:0x654868) + public var tintColor: Color = .init(hex: 0xDF915E) + public var primaryBackgroundColor: Color = .init(hex: 0x433744) + public var secondaryBackgroundColor: Color = .init(hex: 0x654868) public var labelColor: Color = .white - + public init() {} } public struct DesertLight: ColorSet { public var name: ColorSetName = .desertLight public var scheme: ColorScheme = .light - public var tintColor: Color = Color(hex: 0xdf915e) - public var primaryBackgroundColor: Color = Color(hex: 0xfcf2eb) - public var secondaryBackgroundColor: Color = Color(hex:0xeeede7) + public var tintColor: Color = .init(hex: 0xDF915E) + public var primaryBackgroundColor: Color = .init(hex: 0xFCF2EB) + public var secondaryBackgroundColor: Color = .init(hex: 0xEEEDE7) public var labelColor: Color = .black - + public init() {} } public struct NemesisDark: ColorSet { public var name: ColorSetName = .nemesisDark public var scheme: ColorScheme = .dark - public var tintColor: Color = Color(hex: 0x17a2f2) - public var primaryBackgroundColor: Color = Color(hex: 0x000000) - public var secondaryBackgroundColor: Color = Color(hex:0x151e2b) + public var tintColor: Color = .init(hex: 0x17A2F2) + public var primaryBackgroundColor: Color = .init(hex: 0x000000) + public var secondaryBackgroundColor: Color = .init(hex: 0x151E2B) public var labelColor: Color = .white - + public init() {} } public struct NemesisLight: ColorSet { public var name: ColorSetName = .nemesisLight public var scheme: ColorScheme = .light - public var tintColor: Color = Color(hex: 0x17a2f2) - public var primaryBackgroundColor: Color = Color(hex: 0xffffff) - public var secondaryBackgroundColor: Color = Color(hex:0xe8ecef) + public var tintColor: Color = .init(hex: 0x17A2F2) + public var primaryBackgroundColor: Color = .init(hex: 0xFFFFFF) + public var secondaryBackgroundColor: Color = .init(hex: 0xE8ECEF) public var labelColor: Color = .black - + public init() {} } - - - diff --git a/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift b/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift index 9bc9eeee..ba0ed1ce 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift @@ -1,8 +1,8 @@ import Foundation -extension CGFloat { - public static let layoutPadding: CGFloat = 20 - public static let dividerPadding: CGFloat = 2 - public static let statusColumnsSpacing: CGFloat = 8 - public static let maxColumnWidth: CGFloat = 650 +public extension CGFloat { + static let layoutPadding: CGFloat = 20 + static let dividerPadding: CGFloat = 2 + static let statusColumnsSpacing: CGFloat = 8 + static let maxColumnWidth: CGFloat = 650 } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift b/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift index 5732c533..67e1bc0b 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift @@ -1,28 +1,28 @@ import SwiftUI -extension Color { - public static var brand: Color { - Color(red: 187/255, green: 59/255, blue: 226/255) +public extension Color { + static var brand: Color { + Color(red: 187 / 255, green: 59 / 255, blue: 226 / 255) } - - public static var primaryBackground: Color { - Color(red: 16/255, green: 21/255, blue: 35/255) + + static var primaryBackground: Color { + Color(red: 16 / 255, green: 21 / 255, blue: 35 / 255) } - - public static var secondaryBackground: Color { - Color(red: 30/255, green: 35/255, blue: 62/255) + + static var secondaryBackground: Color { + Color(red: 30 / 255, green: 35 / 255, blue: 62 / 255) } - - public static var label: Color { + + static var label: Color { Color("label", bundle: .module) } } extension Color: RawRepresentable { public init?(rawValue: Int) { - let red = Double((rawValue & 0xFF0000) >> 16) / 0xFF + let red = Double((rawValue & 0xFF0000) >> 16) / 0xFF let green = Double((rawValue & 0x00FF00) >> 8) / 0xFF - let blue = Double(rawValue & 0x0000FF) / 0xFF + let blue = Double(rawValue & 0x0000FF) / 0xFF self = Color(red: red, green: green, blue: blue) } @@ -42,11 +42,10 @@ extension Color: RawRepresentable { } extension Color { - init(hex: Int, opacity: Double = 1.0) { - let red = Double((hex & 0xff0000) >> 16) / 255.0 - let green = Double((hex & 0xff00) >> 8) / 255.0 - let blue = Double((hex & 0xff) >> 0) / 255.0 - self.init(.sRGB, red: red, green: green, blue: blue, opacity: opacity) - } + init(hex: Int, opacity: Double = 1.0) { + let red = Double((hex & 0xFF0000) >> 16) / 255.0 + let green = Double((hex & 0xFF00) >> 8) / 255.0 + let blue = Double((hex & 0xFF) >> 0) / 255.0 + self.init(.sRGB, red: red, green: green, blue: blue, opacity: opacity) + } } - diff --git a/Packages/DesignSystem/Sources/DesignSystem/Theme.swift b/Packages/DesignSystem/Sources/DesignSystem/Theme.swift index 4421fcac..cc49dc5e 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Theme.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Theme.swift @@ -7,10 +7,10 @@ public class Theme: ObservableObject { case avatarPosition, avatarShape, statusActionsDisplay, statusDisplayStyle case selectedSet, selectedScheme } - + public enum AvatarPosition: String, CaseIterable { case leading, top - + public var description: LocalizedStringKey { switch self { case .leading: @@ -33,7 +33,7 @@ public class Theme: ObservableObject { } } } - + public enum StatusActionsDisplay: String, CaseIterable { case full, discret, none @@ -48,7 +48,7 @@ public class Theme: ObservableObject { } } } - + public enum StatusDisplayStyle: String, CaseIterable { case large, compact @@ -61,7 +61,7 @@ public class Theme: ObservableObject { } } } - + @AppStorage("is_previously_set") private var isSet: Bool = false @AppStorage(ThemeKey.selectedScheme.rawValue) public var selectedScheme: ColorScheme = .dark @AppStorage(ThemeKey.tint.rawValue) public var tintColor: Color = .black @@ -79,19 +79,19 @@ public class Theme: ObservableObject { @Published public var selectedSet: ColorSetName = .iceCubeDark private var cancellables = Set() - + public static let shared = Theme() - + private init() { selectedSet = storedSet - + // If theme is never set before set the default store. This should only execute once after install. - + if !isSet { setColor(withName: .iceCubeDark) isSet = true } - + avatarPosition = AvatarPosition(rawValue: rawAvatarPosition) ?? .top avatarShape = AvatarShape(rawValue: rawAvatarShape) ?? .rounded @@ -110,7 +110,7 @@ public class Theme: ObservableObject { self?.rawAvatarShape = shape } .store(in: &cancellables) - + // Workaround, since @AppStorage can't be directly observed $selectedSet .dropFirst() @@ -119,7 +119,7 @@ public class Theme: ObservableObject { } .store(in: &cancellables) } - + public static var allColorSet: [ColorSet] { [ IceCubeDark(), @@ -127,17 +127,17 @@ public class Theme: ObservableObject { DesertDark(), DesertLight(), NemesisDark(), - NemesisLight() + NemesisLight(), ] } - + public func setColor(withName name: ColorSetName) { let colorSet = Theme.allColorSet.filter { $0.name == name }.first ?? IceCubeDark() - self.selectedScheme = colorSet.scheme - self.tintColor = colorSet.tintColor - self.primaryBackgroundColor = colorSet.primaryBackgroundColor - self.secondaryBackgroundColor = colorSet.secondaryBackgroundColor - self.labelColor = colorSet.labelColor - self.storedSet = name + selectedScheme = colorSet.scheme + tintColor = colorSet.tintColor + primaryBackgroundColor = colorSet.primaryBackgroundColor + secondaryBackgroundColor = colorSet.secondaryBackgroundColor + labelColor = colorSet.labelColor + storedSet = name } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift b/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift index d698bb00..45958a72 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift @@ -1,68 +1,68 @@ import SwiftUI #if canImport(UIKit) -import UIKit + import UIKit #endif public extension View { - func applyTheme(_ theme: Theme) -> some View { - modifier(ThemeApplier(theme: theme)) - } + func applyTheme(_ theme: Theme) -> some View { + modifier(ThemeApplier(theme: theme)) + } } struct ThemeApplier: ViewModifier { - @ObservedObject var theme: Theme - - func body(content: Content) -> some View { - content - .tint(theme.tintColor) - .preferredColorScheme(theme.selectedScheme == ColorScheme.dark ? .dark : .light) - #if canImport(UIKit) - .onAppear { - setWindowTint(theme.tintColor) - setWindowUserInterfaceStyle(theme.selectedScheme) - setBarsColor(theme.primaryBackgroundColor) - } - .onChange(of: theme.tintColor) { newValue in - setWindowTint(newValue) - } - .onChange(of: theme.selectedScheme) { newValue in - setWindowUserInterfaceStyle(newValue) - } - .onChange(of: theme.primaryBackgroundColor) { newValue in - setBarsColor(newValue) - } - #endif - } - + @ObservedObject var theme: Theme + + func body(content: Content) -> some View { + content + .tint(theme.tintColor) + .preferredColorScheme(theme.selectedScheme == ColorScheme.dark ? .dark : .light) #if canImport(UIKit) - private func setWindowUserInterfaceStyle(_ colorScheme: ColorScheme) { - allWindows() - .forEach { - switch colorScheme { - case .dark: - $0.overrideUserInterfaceStyle = .dark - case .light: - $0.overrideUserInterfaceStyle = .light - } - } - } - - private func setWindowTint(_ color: Color) { - allWindows() - .forEach { - $0.tintColor = UIColor(color) - } - } - - private func setBarsColor(_ color: Color) { - UINavigationBar.appearance().isTranslucent = true - UINavigationBar.appearance().barTintColor = UIColor(color) - } - - private func allWindows() -> [UIWindow] { - UIApplication.shared.connectedScenes - .compactMap { $0 as? UIWindowScene } - .flatMap { $0.windows } - } + .onAppear { + setWindowTint(theme.tintColor) + setWindowUserInterfaceStyle(theme.selectedScheme) + setBarsColor(theme.primaryBackgroundColor) + } + .onChange(of: theme.tintColor) { newValue in + setWindowTint(newValue) + } + .onChange(of: theme.selectedScheme) { newValue in + setWindowUserInterfaceStyle(newValue) + } + .onChange(of: theme.primaryBackgroundColor) { newValue in + setBarsColor(newValue) + } #endif + } + + #if canImport(UIKit) + private func setWindowUserInterfaceStyle(_ colorScheme: ColorScheme) { + allWindows() + .forEach { + switch colorScheme { + case .dark: + $0.overrideUserInterfaceStyle = .dark + case .light: + $0.overrideUserInterfaceStyle = .light + } + } + } + + private func setWindowTint(_ color: Color) { + allWindows() + .forEach { + $0.tintColor = UIColor(color) + } + } + + private func setBarsColor(_ color: Color) { + UINavigationBar.appearance().isTranslucent = true + UINavigationBar.appearance().barTintColor = UIColor(color) + } + + private func allWindows() -> [UIWindow] { + UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + } + #endif } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift index 07d86ef8..12c9272e 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift @@ -1,6 +1,6 @@ -import SwiftUI -import Shimmer import NukeUI +import Shimmer +import SwiftUI public struct AvatarView: View { @Environment(\.redactionReasons) private var reasons @@ -8,7 +8,7 @@ public struct AvatarView: View { public enum Size { case account, status, embed, badge, boost - + public var size: CGSize { switch self { case .account: @@ -23,7 +23,7 @@ public struct AvatarView: View { return .init(width: 12, height: 12) } } - + var cornerRadius: CGFloat { switch self { case .badge, .boost: @@ -33,36 +33,36 @@ public struct AvatarView: View { } } } - + public let url: URL public let size: Size - + public init(url: URL, size: Size = .status) { self.url = url self.size = size } - + public var body: some View { Group { if reasons == .placeholder { RoundedRectangle(cornerRadius: size.cornerRadius) .fill(.gray) .frame(width: size.size.width, height: size.size.height) - } else { - LazyImage(url: url) { state in - if let image = state.image { - image - .resizingMode(.aspectFit) - } else if state.isLoading { - placeholderView - .shimmering() - } else { - placeholderView - } + } else { + LazyImage(url: url) { state in + if let image = state.image { + image + .resizingMode(.aspectFit) + } else if state.isLoading { + placeholderView + .shimmering() + } else { + placeholderView } - .processors([.resize(size: size.size), .roundedCorners(radius: size.cornerRadius)]) - .frame(width: size.size.width, height: size.size.height) } + .processors([.resize(size: size.size), .roundedCorners(radius: size.cornerRadius)]) + .frame(width: size.size.width, height: size.size.height) + } } .clipShape(clipShape) .overlay( @@ -78,7 +78,7 @@ public struct AvatarView: View { return AnyShape(RoundedRectangle(cornerRadius: size.cornerRadius)) } } - + @ViewBuilder private var placeholderView: some View { if size == .badge { diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift index 16d6a818..6ca6745b 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift @@ -1,20 +1,20 @@ -import Foundation import EmojiText -import Models +import Foundation import HTML2Markdown +import Models import SwiftUI public struct EmojiTextApp: View { private let markdown: String private let emojis: [any CustomEmoji] private let append: (() -> Text)? - + public init(_ markdown: HTMLString, emojis: [Emoji], append: (() -> Text)? = nil) { self.markdown = markdown self.emojis = emojis.map { RemoteEmoji(shortcode: $0.shortcode, url: $0.url) } self.append = append } - + public var body: some View { if let append { EmojiText(markdown: markdown, emojis: emojis) diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/EmptyView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/EmptyView.swift index 154164a1..b3294b27 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/EmptyView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/EmptyView.swift @@ -4,13 +4,13 @@ public struct EmptyView: View { public let iconName: String public let title: String public let message: String - + public init(iconName: String, title: String, message: String) { self.iconName = iconName self.title = title self.message = message } - + public var body: some View { VStack { Image(systemName: iconName) diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift index 998afeb7..cbe4292a 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift @@ -4,15 +4,15 @@ public struct ErrorView: View { public let title: String public let message: String public let buttonTitle: String - public let onButtonPress: (() -> Void) - + public let onButtonPress: () -> Void + public init(title: String, message: String, buttonTitle: String, onButtonPress: @escaping (() -> Void)) { self.title = title self.message = message self.buttonTitle = buttonTitle self.onButtonPress = onButtonPress } - + public var body: some View { VStack { Image(systemName: "exclamationmark.triangle.fill") diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/ScrollViewOffsetReader.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/ScrollViewOffsetReader.swift index 52c470d3..71068105 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/ScrollViewOffsetReader.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/ScrollViewOffsetReader.swift @@ -15,7 +15,7 @@ public struct ScrollViewOffsetReader: View { self.onOffsetChange = onOffsetChange self.content = content } - + public var body: some View { ScrollView { offsetReader @@ -40,5 +40,5 @@ public struct ScrollViewOffsetReader: View { private struct OffsetPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = .zero - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {} + static func reduce(value _: inout CGFloat, nextValue _: () -> CGFloat) {} } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift index b01517f7..16750f11 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift @@ -1,10 +1,10 @@ -import SwiftUI import Env import Models +import SwiftUI @MainActor -extension View { - public func statusEditorToolbarItem(routeurPath: RouterPath, visibility: Models.Visibility) -> some ToolbarContent { +public extension View { + func statusEditorToolbarItem(routeurPath: RouterPath, visibility: Models.Visibility) -> some ToolbarContent { ToolbarItem(placement: .navigationBarTrailing) { Button { routeurPath.presentedSheet = .newStatusEditor(visibility: visibility) @@ -18,11 +18,11 @@ extension View { public struct StatusEditorToolbarItem: ToolbarContent { @EnvironmentObject private var routerPath: RouterPath let visibility: Models.Visibility - + public init(visibility: Models.Visibility) { self.visibility = visibility } - + public var body: some ToolbarContent { ToolbarItem(placement: .navigationBarTrailing) { Button { diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift index 65161fdc..7863a889 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift @@ -1,16 +1,16 @@ +import Env import Models import SwiftUI -import Env public struct TagRowView: View { @EnvironmentObject private var routeurPath: RouterPath - + let tag: Tag - + public init(tag: Tag) { self.tag = tag } - + public var body: some View { HStack { VStack(alignment: .leading) { diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/ThemePreviewView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/ThemePreviewView.swift index 58073128..28a3ed7e 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/ThemePreviewView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/ThemePreviewView.swift @@ -1,24 +1,24 @@ -import SwiftUI import Combine +import SwiftUI public struct ThemePreviewView: View { private let gutterSpace: Double = 8 @EnvironmentObject private var theme: Theme @Environment(\.dismiss) var dismiss - + public init() {} - + public var body: some View { ScrollView { - HStack (spacing: gutterSpace) { + HStack(spacing: gutterSpace) { ThemeBoxView(color: IceCubeDark()) ThemeBoxView(color: IceCubeLight()) } - HStack (spacing: gutterSpace) { + HStack(spacing: gutterSpace) { ThemeBoxView(color: DesertDark()) ThemeBoxView(color: DesertLight()) } - HStack (spacing: gutterSpace) { + HStack(spacing: gutterSpace) { ThemeBoxView(color: NemesisDark()) ThemeBoxView(color: NemesisLight()) } @@ -31,13 +31,12 @@ public struct ThemePreviewView: View { } struct ThemeBoxView: View { - @EnvironmentObject var theme: Theme private let gutterSpace = 8.0 @State private var isSelected = false - + var color: ColorSet - + var body: some View { ZStack(alignment: .topTrailing) { Rectangle() @@ -45,19 +44,19 @@ struct ThemeBoxView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .cornerRadius(4) .shadow(radius: 2, x: 2, y: 4) - - VStack (spacing: gutterSpace) { + + VStack(spacing: gutterSpace) { Text(color.name.rawValue) .foregroundColor(color.tintColor) .font(.system(size: 20)) .fontWeight(.bold) - + Text("Toots preview") .foregroundColor(color.labelColor) .frame(maxWidth: .infinity) .padding() .background(color.primaryBackgroundColor) - + Text("#icecube, #techhub") .foregroundColor(color.tintColor) if isSelected { @@ -95,4 +94,3 @@ struct ThemeBoxView: View { } } } - diff --git a/Packages/Env/Package.swift b/Packages/Env/Package.swift index 6f394c79..4a13cbfe 100644 --- a/Packages/Env/Package.swift +++ b/Packages/Env/Package.swift @@ -11,11 +11,12 @@ let package = Package( products: [ .library( name: "Env", - targets: ["Env"]), + targets: ["Env"] + ), ], dependencies: [ .package(name: "Models", path: "../Models"), - .package(name: "Network", path: "../Network") + .package(name: "Network", path: "../Network"), ], targets: [ .target( @@ -23,6 +24,7 @@ let package = Package( dependencies: [ .product(name: "Models", package: "Models"), .product(name: "Network", package: "Network"), - ]), + ] + ), ] ) diff --git a/Packages/Env/Sources/Env/CurrentAccount.swift b/Packages/Env/Sources/Env/CurrentAccount.swift index d0c31ba7..10d9119b 100644 --- a/Packages/Env/Sources/Env/CurrentAccount.swift +++ b/Packages/Env/Sources/Env/CurrentAccount.swift @@ -7,21 +7,21 @@ public class CurrentAccount: ObservableObject { @Published public private(set) var account: Account? @Published public private(set) var lists: [List] = [] @Published public private(set) var tags: [Tag] = [] - + private var client: Client? - - static public let shared = CurrentAccount() - - private init() { } - + + public static let shared = CurrentAccount() + + private init() {} + public func setClient(client: Client) { self.client = client - + Task(priority: .userInitiated) { await fetchUserData() } } - + private func fetchUserData() async { await withTaskGroup(of: Void.self) { group in group.addTask { await self.fetchConnections() } @@ -30,15 +30,15 @@ public class CurrentAccount: ObservableObject { group.addTask { await self.fetchFollowedTags() } } } - + public func fetchConnections() async { guard let client = client else { return } do { let connections: [String] = try await client.get(endpoint: Instances.peers) client.addConnections(connections) - } catch { } + } catch {} } - + public func fetchCurrentAccount() async { guard let client = client, client.isAuth else { account = nil @@ -46,7 +46,7 @@ public class CurrentAccount: ObservableObject { } account = try? await client.get(endpoint: Accounts.verifyCredentials) } - + public func fetchLists() async { guard let client, client.isAuth else { return } do { @@ -55,7 +55,7 @@ public class CurrentAccount: ObservableObject { lists = [] } } - + public func fetchFollowedTags() async { guard let client, client.isAuth else { return } do { @@ -64,15 +64,15 @@ public class CurrentAccount: ObservableObject { tags = [] } } - + public func createList(title: String) async { guard let client else { return } do { let list: Models.List = try await client.post(endpoint: Lists.createList(title: title)) lists.append(list) - } catch { } + } catch {} } - + public func deleteList(list: Models.List) async { guard let client else { return } lists.removeAll(where: { $0.id == list.id }) @@ -81,7 +81,7 @@ public class CurrentAccount: ObservableObject { lists.append(list) } } - + public func followTag(id: String) async -> Tag? { guard let client else { return nil } do { @@ -92,12 +92,12 @@ public class CurrentAccount: ObservableObject { return nil } } - + public func unfollowTag(id: String) async -> Tag? { guard let client else { return nil } do { let tag: Tag = try await client.post(endpoint: Tags.unfollow(id: id)) - tags.removeAll{ $0.id == tag.id } + tags.removeAll { $0.id == tag.id } return tag } catch { return nil diff --git a/Packages/Env/Sources/Env/CurrentInstance.swift b/Packages/Env/Sources/Env/CurrentInstance.swift index 7aa1b498..8d077b6c 100644 --- a/Packages/Env/Sources/Env/CurrentInstance.swift +++ b/Packages/Env/Sources/Env/CurrentInstance.swift @@ -5,20 +5,20 @@ import Network @MainActor public class CurrentInstance: ObservableObject { @Published public private(set) var instance: Instance? - + private var client: Client? - - static public let shared = CurrentInstance() - - private init() { } - + + public static let shared = CurrentInstance() + + private init() {} + public func setClient(client: Client) { self.client = client Task { await fetchCurrentInstance() } } - + public func fetchCurrentInstance() async { guard let client = client else { return } Task { diff --git a/Packages/Env/Sources/Env/Ext/AppStorage.swift b/Packages/Env/Sources/Env/Ext/AppStorage.swift index 20676120..39a3eba3 100644 --- a/Packages/Env/Sources/Env/Ext/AppStorage.swift +++ b/Packages/Env/Sources/Env/Ext/AppStorage.swift @@ -9,7 +9,7 @@ extension Array: RawRepresentable where Element: Codable { } self = result } - + public var rawValue: String { guard let data = try? JSONEncoder().encode(self), let result = String(data: data, encoding: .utf8) diff --git a/Packages/Env/Sources/Env/PreferredBrowser.swift b/Packages/Env/Sources/Env/PreferredBrowser.swift index 87bba644..f9722383 100644 --- a/Packages/Env/Sources/Env/PreferredBrowser.swift +++ b/Packages/Env/Sources/Env/PreferredBrowser.swift @@ -1,6 +1,6 @@ import Foundation public enum PreferredBrowser: Int, CaseIterable { - case inAppSafari - case safari + case inAppSafari + case safari } diff --git a/Packages/Env/Sources/Env/PushNotificationsService.swift b/Packages/Env/Sources/Env/PushNotificationsService.swift index 3da9f29a..b38d529e 100644 --- a/Packages/Env/Sources/Env/PushNotificationsService.swift +++ b/Packages/Env/Sources/Env/PushNotificationsService.swift @@ -1,10 +1,10 @@ -import Foundation -import UserNotifications -import SwiftUI -import KeychainSwift import CryptoKit +import Foundation +import KeychainSwift import Models import Network +import SwiftUI +import UserNotifications @MainActor public class PushNotificationsService: ObservableObject { @@ -13,21 +13,21 @@ public class PushNotificationsService: ObservableObject { static let keychainAuthKey = "notifications_auth_key" static let keychainPrivateKey = "notifications_private_key" } - + public struct PushAccounts { public let server: String public let token: OauthToken - + public init(server: String, token: OauthToken) { self.server = server self.token = token } } - + public static let shared = PushNotificationsService() - + @Published public var pushToken: Data? - + @AppStorage("user_push_is_on") public var isUserPushEnabled: Bool = true @Published public var isPushEnabled: Bool = false { didSet { @@ -36,15 +36,16 @@ public class PushNotificationsService: ObservableObject { } } } + @Published public var isFollowNotificationEnabled: Bool = true @Published public var isFavoriteNotificationEnabled: Bool = true @Published public var isReblogNotificationEnabled: Bool = true @Published public var isMentionNotificationEnabled: Bool = true @Published public var isPollNotificationEnabled: Bool = true @Published public var isNewPostsNotificationEnabled: Bool = true - + private var subscriptions: [PushSubscription] = [] - + private var keychain: KeychainSwift { let keychain = KeychainSwift() #if !DEBUG @@ -52,15 +53,15 @@ public class PushNotificationsService: ObservableObject { #endif return keychain } - + public func requestPushNotifications() { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (_, _) in + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } - + public func fetchSubscriptions(accounts: [PushAccounts]) async { subscriptions = [] for account in accounts { @@ -68,11 +69,11 @@ public class PushNotificationsService: ObservableObject { do { let sub: PushSubscription = try await client.get(endpoint: Push.subscription) subscriptions.append(sub) - } catch { } + } catch {} } refreshSubscriptionsUI() } - + public func updateSubscriptions(accounts: [PushAccounts]) async { subscriptions = [] let key = notificationsPrivateKeyAsKey.publicKey.x963Representation @@ -80,15 +81,15 @@ public class PushNotificationsService: ObservableObject { guard let pushToken = pushToken, isUserPushEnabled else { return } for account in accounts { let client = Client(server: account.server, oauthToken: account.token) - do { - var listenerURL = Constants.endpoint - listenerURL += "/push/" - listenerURL += pushToken.hexString - listenerURL += "/\(account.server)" - #if DEBUG - listenerURL += "?sandbox=true" - #endif - let sub: PushSubscription = + do { + var listenerURL = Constants.endpoint + listenerURL += "/push/" + listenerURL += pushToken.hexString + listenerURL += "/\(account.server)" + #if DEBUG + listenerURL += "?sandbox=true" + #endif + let sub: PushSubscription = try await client.post(endpoint: Push.createSub(endpoint: listenerURL, p256dh: key, auth: authKey, @@ -98,23 +99,23 @@ public class PushNotificationsService: ObservableObject { follow: isFollowNotificationEnabled, favourite: isFavoriteNotificationEnabled, poll: isPollNotificationEnabled)) - subscriptions.append(sub) - } catch { } - } + subscriptions.append(sub) + } catch {} + } refreshSubscriptionsUI() } - + public func deleteSubscriptions(accounts: [PushAccounts]) async { for account in accounts { let client = Client(server: account.server, oauthToken: account.token) do { _ = try await client.delete(endpoint: Push.subscription) - } catch { } + } catch {} } await fetchSubscriptions(accounts: accounts) refreshSubscriptionsUI() } - + private func refreshSubscriptionsUI() { if let sub = subscriptions.first { isPushEnabled = true @@ -128,12 +129,13 @@ public class PushNotificationsService: ObservableObject { isPushEnabled = false } } - + // MARK: - Key management - + 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 { @@ -151,10 +153,11 @@ public class PushNotificationsService: ObservableObject { 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.makeRandomeNotificationsAuthKey() @@ -164,8 +167,8 @@ public class PushNotificationsService: ObservableObject { return key } } - - static private func makeRandomeNotificationsAuthKey() -> Data { + + private static func makeRandomeNotificationsAuthKey() -> Data { let byteCount = 16 var bytes = Data(count: byteCount) _ = bytes.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, byteCount, $0.baseAddress!) } @@ -178,4 +181,3 @@ extension Data { return map { String(format: "%02.2hhx", arguments: [$0]) }.joined() } } - diff --git a/Packages/Env/Sources/Env/QuickLook.swift b/Packages/Env/Sources/Env/QuickLook.swift index 5db839d9..d9d03455 100644 --- a/Packages/Env/Sources/Env/QuickLook.swift +++ b/Packages/Env/Sources/Env/QuickLook.swift @@ -7,11 +7,9 @@ public class QuickLook: ObservableObject { @Published public private(set) var urls: [URL] = [] @Published public private(set) var isPreparing: Bool = false @Published public private(set) var latestError: Error? - - public init() { - - } - + + public init() {} + public func prepareFor(urls: [URL], selectedURL: URL) async { withAnimation { isPreparing = true @@ -43,7 +41,7 @@ public class QuickLook: ObservableObject { latestError = error } } - + private func localPathFor(url: URL) async throws -> URL { let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) let path = tempDir.appendingPathComponent(url.lastPathComponent) diff --git a/Packages/Env/Sources/Env/Routeur.swift b/Packages/Env/Sources/Env/Routeur.swift index 4752e5e2..e2dbe575 100644 --- a/Packages/Env/Sources/Env/Routeur.swift +++ b/Packages/Env/Sources/Env/Routeur.swift @@ -1,7 +1,7 @@ import Foundation -import SwiftUI import Models import Network +import SwiftUI public enum RouteurDestinations: Hashable { case accountDetail(id: String) @@ -26,7 +26,7 @@ public enum SheetDestinations: Identifiable { case listAddAccount(account: Account) case addAccount case addRemoteLocalTimeline - + public var id: String { switch self { case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor, .mentionStatusEditor: @@ -47,19 +47,20 @@ public enum SheetDestinations: Identifiable { public class RouterPath: ObservableObject { public var client: Client? public var urlHandler: ((URL) -> OpenURLAction.Result)? - + @Published public var path: [RouteurDestinations] = [] @Published public var presentedSheet: SheetDestinations? - + public init() {} - + public func navigate(to: RouteurDestinations) { path.append(to) } - + public func handleStatus(status: AnyStatus, 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 let mention = status.mentions.first(where: { $0.url == url }) { @@ -68,7 +69,8 @@ public class RouterPath: ObservableObject { } else if let client = client, client.isAuth, client.hasConnection(with: url), - let id = Int(url.lastPathComponent) { + let id = Int(url.lastPathComponent) + { if url.absoluteString.contains(client.server) { navigate(to: .statusDetail(id: String(id))) } else { @@ -78,10 +80,11 @@ public class RouterPath: ObservableObject { } return urlHandler?(url) ?? .systemAction } - + 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 == "@", let host = url.host { @@ -93,7 +96,7 @@ public class RouterPath: ObservableObject { } return urlHandler?(url) ?? .systemAction } - + public func navigateToAccountFrom(acct: String, url: URL) async { guard let client else { return } Task { @@ -109,7 +112,7 @@ public class RouterPath: ObservableObject { } } } - + public func navigateToAccountFrom(url: URL) async { guard let client else { return } Task { diff --git a/Packages/Env/Sources/Env/StreamWatcher.swift b/Packages/Env/Sources/Env/StreamWatcher.swift index 359e2c6c..aabd5b16 100644 --- a/Packages/Env/Sources/Env/StreamWatcher.swift +++ b/Packages/Env/Sources/Env/StreamWatcher.swift @@ -3,28 +3,28 @@ import Models import Network @MainActor -public class StreamWatcher: ObservableObject { +public class StreamWatcher: ObservableObject { private var client: Client? private var task: URLSessionWebSocketTask? private var watchedStreams: [Stream] = [] - + private let decoder = JSONDecoder() private let encoder = JSONEncoder() - + public enum Stream: String { case publicTimeline = "public" case user case direct } - + @Published public var events: [any StreamEvent] = [] @Published public var unreadNotificationsCount: Int = 0 @Published public var latestEvent: (any StreamEvent)? - + public init() { decoder.keyDecodingStrategy = .convertFromSnakeCase } - + public func setClient(client: Client) { if self.client != nil { stopWatching() @@ -32,13 +32,13 @@ public class StreamWatcher: ObservableObject { self.client = client connect() } - + private func connect() { task = client?.makeWebSocketTask(endpoint: Streaming.streaming) task?.resume() receiveMessage() } - + public func watch(streams: [Stream]) { if client?.isAuth == false { return @@ -51,17 +51,17 @@ public class StreamWatcher: ObservableObject { sendMessage(message: StreamMessage(type: "subscribe", stream: stream.rawValue)) } } - + public func stopWatching() { task?.cancel() task = nil } - + private func sendMessage(message: StreamMessage) { task?.send(.data(try! encoder.encode(message)), completionHandler: { _ in }) } - + private func receiveMessage() { task?.receive(completionHandler: { result in switch result { @@ -86,13 +86,13 @@ public class StreamWatcher: ObservableObject { } catch { print("Error decoding streaming event: \(error.localizedDescription)") } - + default: break } - - self.receiveMessage() - + + self.receiveMessage() + case .failure: DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10)) { [weak self] in guard let self = self else { return } @@ -103,7 +103,7 @@ public class StreamWatcher: ObservableObject { } }) } - + private func rawEventToEvent(rawEvent: RawStreamEvent) -> (any StreamEvent)? { guard let payloadData = rawEvent.payload.data(using: .utf8) else { return nil diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index 1676f03c..657a1314 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -1,19 +1,19 @@ -import SwiftUI import Foundation import Models import Network +import SwiftUI @MainActor public class UserPreferences: ObservableObject { - public static let sharedDefault = UserDefaults.init(suiteName: "group.icecubesapps") + public static let sharedDefault = UserDefaults(suiteName: "group.icecubesapps") public static let shared = UserPreferences() - + private var client: Client? - + @AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = [] @AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari @AppStorage("draft_posts") public var draftsPosts: [String] = [] - + public var pushNotificationsCount: Int { get { Self.sharedDefault?.integer(forKey: "push_notifications_count") ?? 0 @@ -22,18 +22,18 @@ public class UserPreferences: ObservableObject { Self.sharedDefault?.set(newValue, forKey: "push_notifications_count") } } - + @Published public var serverPreferences: ServerPreferences? - - private init() { } - + + private init() {} + public func setClient(client: Client) { self.client = client Task { await refreshServerPreferences() } } - + public func refreshServerPreferences() async { guard let client, client.isAuth else { return } serverPreferences = try? await client.get(endpoint: Accounts.preferences) diff --git a/Packages/Explore/Package.swift b/Packages/Explore/Package.swift index 850e1ea4..c4c5e99e 100644 --- a/Packages/Explore/Package.swift +++ b/Packages/Explore/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "Explore", - targets: ["Explore"]), + targets: ["Explore"] + ), ], dependencies: [ .package(name: "Account", path: "../Account"), @@ -30,8 +31,8 @@ let package = Package( .product(name: "Models", package: "Models"), .product(name: "Env", package: "Env"), .product(name: "Status", package: "Status"), - .product(name: "DesignSystem", package: "DesignSystem") - ]) + .product(name: "DesignSystem", package: "DesignSystem"), + ] + ), ] ) - diff --git a/Packages/Explore/Sources/Explore/ExploreView.swift b/Packages/Explore/Sources/Explore/ExploreView.swift index e4b2d525..3c6efa99 100644 --- a/Packages/Explore/Sources/Explore/ExploreView.swift +++ b/Packages/Explore/Sources/Explore/ExploreView.swift @@ -1,21 +1,21 @@ -import SwiftUI -import Env -import Network -import DesignSystem -import Models -import Status -import Shimmer import Account +import DesignSystem +import Env +import Models +import Network +import Shimmer +import Status +import SwiftUI public struct ExploreView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var client: Client @EnvironmentObject private var routeurPath: RouterPath - + @StateObject private var viewModel = ExploreViewModel() - - public init() { } - + + public init() {} + public var body: some View { List { if !viewModel.searchQuery.isEmpty { @@ -30,7 +30,7 @@ public struct ExploreView: View { EmptyView(iconName: "magnifyingglass", title: "Search your instance", message: "From this screen you can search anything on \(client.server)") - .listRowBackground(theme.secondaryBackgroundColor) + .listRowBackground(theme.secondaryBackgroundColor) } else { if !viewModel.trendingTags.isEmpty { trendingTagsSection @@ -64,10 +64,10 @@ public struct ExploreView: View { suggestedTokens: $viewModel.suggestedToken, prompt: Text("Search users, posts and tags"), token: { token in - Text(token.rawValue) - }) + Text(token.rawValue) + }) } - + private var loadingView: some View { ForEach(Status.placeholders()) { status in StatusRowView(viewModel: .init(status: status, isCompact: false)) @@ -77,7 +77,7 @@ public struct ExploreView: View { .listRowBackground(theme.primaryBackgroundColor) } } - + @ViewBuilder private func makeSearchResultsView(results: SearchResults) -> some View { if !results.accounts.isEmpty { @@ -109,16 +109,16 @@ public struct ExploreView: View { } } } - + private var suggestedAccountsSection: some View { 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 }) { - AccountsListRow(viewModel: .init(account: account, relationShip: relationship)) - .listRowBackground(theme.primaryBackgroundColor) + if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) { + AccountsListRow(viewModel: .init(account: account, relationShip: relationship)) + .listRowBackground(theme.primaryBackgroundColor) + } } - } NavigationLink { List { ForEach(viewModel.suggestedAccounts) { account in @@ -140,15 +140,15 @@ public struct ExploreView: View { .listRowBackground(theme.primaryBackgroundColor) } } - + private var trendingTagsSection: some View { Section("Trending Tags") { ForEach(viewModel.trendingTags .prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in - TagRowView(tag: tag) + TagRowView(tag: tag) .listRowBackground(theme.primaryBackgroundColor) - .padding(.vertical, 4) - } + .padding(.vertical, 4) + } NavigationLink { List { ForEach(viewModel.trendingTags) { tag in @@ -169,16 +169,16 @@ public struct ExploreView: View { .listRowBackground(theme.primaryBackgroundColor) } } - + private var trendingPostsSection: some View { Section("Trending Posts") { ForEach(viewModel.trendingStatuses .prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in - StatusRowView(viewModel: .init(status: status, isCompact: false)) + StatusRowView(viewModel: .init(status: status, isCompact: false)) .listRowBackground(theme.primaryBackgroundColor) - .padding(.vertical, 8) - } - + .padding(.vertical, 8) + } + NavigationLink { List { ForEach(viewModel.trendingStatuses) { status in @@ -199,15 +199,15 @@ public struct ExploreView: View { .listRowBackground(theme.primaryBackgroundColor) } } - + private var trendingLinksSection: some View { Section("Trending Links") { ForEach(viewModel.trendingLinks .prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in - StatusCardView(card: card) + StatusCardView(card: card) .listRowBackground(theme.primaryBackgroundColor) - .padding(.vertical, 8) - } + .padding(.vertical, 8) + } NavigationLink { List { ForEach(viewModel.trendingLinks) { card in @@ -228,5 +228,4 @@ public struct ExploreView: View { .listRowBackground(theme.primaryBackgroundColor) } } - } diff --git a/Packages/Explore/Sources/Explore/ExploreViewModel.swift b/Packages/Explore/Sources/Explore/ExploreViewModel.swift index 94f5ac42..61f63f1b 100644 --- a/Packages/Explore/Sources/Explore/ExploreViewModel.swift +++ b/Packages/Explore/Sources/Explore/ExploreViewModel.swift @@ -1,7 +1,7 @@ -import SwiftUI +import Combine import Models import Network -import Combine +import SwiftUI @MainActor class ExploreViewModel: ObservableObject { @@ -17,16 +17,16 @@ class ExploreViewModel: ObservableObject { } } } - + enum Token: String, Identifiable { case user = "@user" case statuses = "@posts" case tag = "#hashtag" - + var id: String { rawValue } - + var apiType: String { switch self { case .user: @@ -42,7 +42,7 @@ class ExploreViewModel: ObservableObject { var allSectionsEmpty: Bool { trendingLinks.isEmpty && trendingTags.isEmpty && trendingStatuses.isEmpty && suggestedAccounts.isEmpty } - + @Published var tokens: [Token] = [] @Published var suggestedToken: [Token] = [] @Published var searchQuery = "" @@ -53,7 +53,7 @@ class ExploreViewModel: ObservableObject { @Published var trendingTags: [Tag] = [] @Published var trendingStatuses: [Status] = [] @Published var trendingLinks: [Card] = [] - + private var searchTask: Task? private var cancellables = Set() @@ -61,7 +61,7 @@ class ExploreViewModel: ObservableObject { $searchQuery .removeDuplicates() .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) - .sink(receiveValue: { [weak self] newValue in + .sink(receiveValue: { [weak self] _ in guard let self else { return } if self.searchQuery.starts(with: "@") { @@ -76,17 +76,17 @@ class ExploreViewModel: ObservableObject { }) .store(in: &cancellables) } - + func fetchTrending() async { guard let client else { return } do { let data = try await fetchTrendingsData(client: client) - self.suggestedAccounts = data.suggestedAccounts - self.trendingTags = data.trendingTags - self.trendingStatuses = data.trendingStatuses - self.trendingLinks = data.trendingLinks - - self.suggestedAccountsRelationShips = try await client.get(endpoint: Accounts.relationships(ids: self.suggestedAccounts.map{ $0.id })) + suggestedAccounts = data.suggestedAccounts + trendingTags = data.trendingTags + trendingStatuses = data.trendingStatuses + trendingLinks = data.trendingLinks + + suggestedAccountsRelationShips = try await client.get(endpoint: Accounts.relationships(ids: suggestedAccounts.map { $0.id })) withAnimation { isLoaded = true } @@ -94,14 +94,14 @@ class ExploreViewModel: ObservableObject { isLoaded = true } } - + private struct TrendingData { let suggestedAccounts: [Account] let trendingTags: [Tag] let trendingStatuses: [Status] let trendingLinks: [Card] } - + private func fetchTrendingsData(client: Client) async throws -> TrendingData { async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions) async let trendingTags: [Tag] = client.get(endpoint: Trends.tags) @@ -112,7 +112,7 @@ class ExploreViewModel: ObservableObject { trendingStatuses: trendingStatuses, trendingLinks: trendingLinks) } - + func search() { guard !searchQuery.isEmpty else { return } searchTask?.cancel() @@ -127,10 +127,10 @@ class ExploreViewModel: ObservableObject { following: nil), forceVersion: .v2) let relationships: [Relationshionship] = - try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map{ $0.id })) + try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map { $0.id })) results.relationships = relationships self.results[searchQuery] = results - } catch { } + } catch {} } } } diff --git a/Packages/Lists/Package.swift b/Packages/Lists/Package.swift index c2eddc3f..8029b3fc 100644 --- a/Packages/Lists/Package.swift +++ b/Packages/Lists/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "Lists", - targets: ["Lists"]), + targets: ["Lists"] + ), ], dependencies: [ .package(name: "Network", path: "../Network"), @@ -26,8 +27,8 @@ let package = Package( .product(name: "Network", package: "Network"), .product(name: "Models", package: "Models"), .product(name: "Env", package: "Env"), - .product(name: "DesignSystem", package: "DesignSystem") - ]), + .product(name: "DesignSystem", package: "DesignSystem"), + ] + ), ] ) - diff --git a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift index 25e6a14a..e11e3866 100644 --- a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift +++ b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift @@ -1,8 +1,8 @@ -import SwiftUI -import Network import DesignSystem import Env import Models +import Network +import SwiftUI public struct ListAddAccountView: View { @Environment(\.dismiss) private var dismiss @@ -10,15 +10,14 @@ public struct ListAddAccountView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var currentAccount: CurrentAccount @StateObject private var viewModel: ListAddAccountViewModel - + @State private var isCreateListAlertPresented: Bool = false @State private var createListTitle: String = "" - - + public init(account: Account) { _viewModel = StateObject(wrappedValue: .init(account: account)) } - + public var body: some View { NavigationStack { List { diff --git a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift index 7139e211..910ae16a 100644 --- a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift +++ b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift @@ -1,20 +1,20 @@ -import SwiftUI import Models import Network +import SwiftUI @MainActor class ListAddAccountViewModel: ObservableObject { let account: Account - + @Published var inLists: [Models.List] = [] @Published var isLoadingInfo: Bool = true - + var client: Client? - + init(account: Account) { self.account = account } - + func fetchInfo() async { guard let client else { return } isLoadingInfo = true @@ -27,7 +27,7 @@ class ListAddAccountViewModel: ObservableObject { } } } - + 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])) @@ -35,7 +35,7 @@ class ListAddAccountViewModel: ObservableObject { inLists.append(list) } } - + 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])) diff --git a/Packages/Lists/Sources/Lists/Edit/ListEditView.swift b/Packages/Lists/Sources/Lists/Edit/ListEditView.swift index fa641c4d..49050a50 100644 --- a/Packages/Lists/Sources/Lists/Edit/ListEditView.swift +++ b/Packages/Lists/Sources/Lists/Edit/ListEditView.swift @@ -1,20 +1,20 @@ -import SwiftUI -import Models import DesignSystem -import Network import EmojiText +import Models +import Network +import SwiftUI public struct ListEditView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var theme: Theme @EnvironmentObject private var client: Client - + @StateObject private var viewModel: ListEditViewModel - + public init(list: Models.List) { _viewModel = StateObject(wrappedValue: .init(list: list)) } - + public var body: some View { NavigationStack { List { diff --git a/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift b/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift index c5aecd3b..cac0b84c 100644 --- a/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift +++ b/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift @@ -1,20 +1,20 @@ -import SwiftUI import Models import Network +import SwiftUI @MainActor public class ListEditViewModel: ObservableObject { let list: Models.List - + var client: Client? - + @Published var isLoadingAccounts: Bool = true @Published var accounts: [Account] = [] - + init(list: Models.List) { self.list = list } - + func fetchAccounts() async { guard let client else { return } isLoadingAccounts = true @@ -25,14 +25,14 @@ public class ListEditViewModel: ObservableObject { isLoadingAccounts = false } } - + func delete(account: Account) async { guard let client else { return } do { - 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 }) } - } catch { } + } catch {} } } diff --git a/Packages/Models/Package.swift b/Packages/Models/Package.swift index af87ad0f..5c07da93 100644 --- a/Packages/Models/Package.swift +++ b/Packages/Models/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "Models", - targets: ["Models"]), + targets: ["Models"] + ), ], dependencies: [ .package(url: "https://gitlab.com/mflint/HTML2Markdown", exact: "1.0.0"), @@ -21,9 +22,11 @@ let package = Package( .target( name: "Models", dependencies: ["HTML2Markdown", - "SwiftSoup"]), + "SwiftSoup"] + ), .testTarget( name: "ModelsTests", - dependencies: ["Models"]), + dependencies: ["Models"] + ), ] ) diff --git a/Packages/Models/Sources/Models/Account.swift b/Packages/Models/Sources/Models/Account.swift index 861495ea..9818abc1 100644 --- a/Packages/Models/Sources/Models/Account.swift +++ b/Packages/Models/Sources/Models/Account.swift @@ -1,21 +1,20 @@ import Foundation public struct Account: Codable, Identifiable, Equatable, Hashable { - public func hash(into hasher: inout Hasher) { hasher.combine(id) } - + public struct Field: Codable, Equatable, Identifiable { public var id: String { value + name } - + public let name: String public let value: HTMLString public let verifiedAt: String? } - + public struct Source: Codable, Equatable { public let privacy: Visibility public let sensitive: Bool @@ -23,7 +22,7 @@ public struct Account: Codable, Identifiable, Equatable, Hashable { public let note: String public let fields: [Field] } - + public let id: String public let username: String public let displayName: String @@ -43,7 +42,7 @@ public struct Account: Codable, Identifiable, Equatable, Hashable { public let source: Source? public let bot: Bool public let discoverable: Bool? - + public static func placeholder() -> Account { .init(id: UUID().uuidString, username: "Username", @@ -65,7 +64,7 @@ public struct Account: Codable, Identifiable, Equatable, Hashable { bot: false, discoverable: true) } - + public static func placeholders() -> [Account] { [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] diff --git a/Packages/Models/Sources/Models/Alias/HTMLString.swift b/Packages/Models/Sources/Models/Alias/HTMLString.swift index e5f5792e..f1855029 100644 --- a/Packages/Models/Sources/Models/Alias/HTMLString.swift +++ b/Packages/Models/Sources/Models/Alias/HTMLString.swift @@ -5,8 +5,8 @@ import SwiftUI public typealias HTMLString = String -extension HTMLString { - public var asMarkdown: String { +public extension HTMLString { + var asMarkdown: String { do { let dom = try HTMLParser().parse(html: self) return dom.toMarkdown() @@ -16,8 +16,8 @@ extension HTMLString { return self } } - - public var asRawText: String { + + var asRawText: String { do { let document: Document = try SwiftSoup.parse(self) return try document.text() @@ -25,8 +25,8 @@ extension HTMLString { return self } } - - public func findStatusesURLs() -> [URL]? { + + func findStatusesURLs() -> [URL]? { do { let document: Document = try SwiftSoup.parse(self) let links: Elements = try document.select("a") @@ -34,7 +34,8 @@ extension HTMLString { for link in links { let href = try link.attr("href") if let url = URL(string: href), - let _ = Int(url.lastPathComponent) { + let _ = Int(url.lastPathComponent) + { URLs.append(url) } } @@ -43,8 +44,8 @@ extension HTMLString { return nil } } - - public var asSafeAttributedString: AttributedString { + + var asSafeAttributedString: AttributedString { do { let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true, interpretedSyntax: .inlineOnlyPreservingWhitespace) @@ -54,4 +55,3 @@ extension HTMLString { } } } - diff --git a/Packages/Models/Sources/Models/Alias/ServerDate.swift b/Packages/Models/Sources/Models/Alias/ServerDate.swift index ff52cae6..7df46793 100644 --- a/Packages/Models/Sources/Models/Alias/ServerDate.swift +++ b/Packages/Models/Sources/Models/Alias/ServerDate.swift @@ -10,23 +10,23 @@ extension ServerDate { dateFormatter.timeZone = .init(abbreviation: "UTC") return dateFormatter } - + private static var createdAtRelativeFormatter: RelativeDateTimeFormatter { let dateFormatter = RelativeDateTimeFormatter() dateFormatter.unitsStyle = .abbreviated return dateFormatter } - + private static var createdAtShortDateFormatted: DateFormatter { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium return dateFormatter } - + public var asDate: Date { Self.createdAtDateFormatter.date(from: self)! } - + public var formatted: String { let calendar = Calendar(identifier: .gregorian) if calendar.numberOfDaysBetween(asDate, and: Date()) > 1 { @@ -37,13 +37,12 @@ extension ServerDate { } } - extension Calendar { func numberOfDaysBetween(_ from: Date, and to: Date) -> Int { let fromDate = startOfDay(for: from) let toDate = startOfDay(for: to) let numberOfDays = dateComponents([.day], from: fromDate, to: toDate) - + return numberOfDays.day! } } diff --git a/Packages/Models/Sources/Models/App/App.swift b/Packages/Models/Sources/Models/App/App.swift index 5dc5e59d..faad4e44 100644 --- a/Packages/Models/Sources/Models/App/App.swift +++ b/Packages/Models/Sources/Models/App/App.swift @@ -1,6 +1,6 @@ import Foundation -public struct AppInfo { +public enum AppInfo { public static let clientName = "IceCubesApp" public static let scheme = "icecubesapp://" public static let scopes = "read write follow push" diff --git a/Packages/Models/Sources/Models/Card.swift b/Packages/Models/Sources/Models/Card.swift index cf26de38..78f52df4 100644 --- a/Packages/Models/Sources/Models/Card.swift +++ b/Packages/Models/Sources/Models/Card.swift @@ -4,7 +4,7 @@ public struct Card: Codable, Identifiable { public var id: String { url.absoluteString } - + public let url: URL public let title: String? public let description: String? diff --git a/Packages/Models/Sources/Models/Conversation.swift b/Packages/Models/Sources/Models/Conversation.swift index cd75130f..5050d5e8 100644 --- a/Packages/Models/Sources/Models/Conversation.swift +++ b/Packages/Models/Sources/Models/Conversation.swift @@ -5,11 +5,11 @@ public struct Conversation: Identifiable, Decodable { public let unread: Bool public let lastStatus: Status public let accounts: [Account] - + public static func placeholder() -> Conversation { .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()] diff --git a/Packages/Models/Sources/Models/Emoji.swift b/Packages/Models/Sources/Models/Emoji.swift index 60a8cf53..d0033eac 100644 --- a/Packages/Models/Sources/Models/Emoji.swift +++ b/Packages/Models/Sources/Models/Emoji.swift @@ -1,15 +1,14 @@ import Foundation public struct Emoji: Codable, Hashable, Identifiable { - public func hash(into hasher: inout Hasher) { hasher.combine(shortcode) } - + public var id: String { shortcode } - + public let shortcode: String public let url: URL public let staticUrl: URL diff --git a/Packages/Models/Sources/Models/Filter.swift b/Packages/Models/Sources/Models/Filter.swift index c8c8abf0..648fecd7 100644 --- a/Packages/Models/Sources/Models/Filter.swift +++ b/Packages/Models/Sources/Models/Filter.swift @@ -9,12 +9,12 @@ public struct Filter: Codable, Identifiable { public enum Action: String, Codable { case warn, hide } - + public enum Context: String, Codable { case home, notifications, account, thread case pub = "public" } - + public let id: String public let title: String public let context: [String] diff --git a/Packages/Models/Sources/Models/Instance.swift b/Packages/Models/Sources/Models/Instance.swift index 1a470a4e..9b89060e 100644 --- a/Packages/Models/Sources/Models/Instance.swift +++ b/Packages/Models/Sources/Models/Instance.swift @@ -6,29 +6,29 @@ public struct Instance: Codable { public let statusCount: Int public let domainCount: Int } - + public struct Configuration: Codable { public struct Statuses: Codable { public let maxCharacters: Int public let maxMediaAttachments: Int } - + public struct Polls: Codable { public let maxOptions: Int public let maxCharactersPerOption: Int public let minExpiration: Int public let maxExpiration: Int } - + public let statuses: Statuses public let polls: Polls } - + public struct Rule: Codable, Identifiable { public let id: String public let text: String } - + public let title: String public let shortDescription: String public let email: String diff --git a/Packages/Models/Sources/Models/InstanceSocial.swift b/Packages/Models/Sources/Models/InstanceSocial.swift index 896de501..6325ce23 100644 --- a/Packages/Models/Sources/Models/InstanceSocial.swift +++ b/Packages/Models/Sources/Models/InstanceSocial.swift @@ -4,6 +4,7 @@ public struct InstanceSocial: Decodable, Identifiable { public struct Info: Decodable { public let shortDescription: String } + public let id: String public let name: String public let dead: Bool diff --git a/Packages/Models/Sources/Models/MastodonPushNotification.swift b/Packages/Models/Sources/Models/MastodonPushNotification.swift index 0dc8e59a..54c983b3 100644 --- a/Packages/Models/Sources/Models/MastodonPushNotification.swift +++ b/Packages/Models/Sources/Models/MastodonPushNotification.swift @@ -1,17 +1,16 @@ import Foundation public struct MastodonPushNotification: Codable { - public let accessToken: String - + public let notificationID: Int public let notificationType: String - + public let preferredLocale: String? public let icon: String? public let title: String public let body: String - + enum CodingKeys: String, CodingKey { case accessToken = "access_token" case notificationID = "notification_id" diff --git a/Packages/Models/Sources/Models/MediaAttachement.swift b/Packages/Models/Sources/Models/MediaAttachement.swift index 3a98a513..47b6b47d 100644 --- a/Packages/Models/Sources/Models/MediaAttachement.swift +++ b/Packages/Models/Sources/Models/MediaAttachement.swift @@ -1,31 +1,31 @@ import Foundation public struct MediaAttachement: Codable, Identifiable, Hashable { - public struct MetaContainer: Codable, Equatable { public struct Meta: Codable, Equatable { public let width: Int? public let height: Int? } + public let original: Meta? } - + public enum SupportedType: String { case image, gifv, video, audio } - + public func hash(into hasher: inout Hasher) { hasher.combine(id) } - + public let id: String public let type: String public var supportedType: SupportedType? { SupportedType(rawValue: type) } + public let url: URL? public let previewUrl: URL? public let description: String? public let meta: MetaContainer? } - diff --git a/Packages/Models/Sources/Models/Notification.swift b/Packages/Models/Sources/Models/Notification.swift index eb2c4bfe..78546d5a 100644 --- a/Packages/Models/Sources/Models/Notification.swift +++ b/Packages/Models/Sources/Models/Notification.swift @@ -4,17 +4,17 @@ public struct Notification: Codable, Identifiable { public enum NotificationType: String, CaseIterable { case follow, follow_request, mention, reblog, status, favourite, poll, update } - + public let id: String public let type: String public let createdAt: ServerDate public let account: Account public let status: Status? - + public var supportedType: NotificationType? { .init(rawValue: type) } - + public static func placeholder() -> Notification { .init(id: UUID().uuidString, type: NotificationType.favourite.rawValue, @@ -22,9 +22,8 @@ public struct Notification: Codable, Identifiable { account: .placeholder(), status: .placeholder()) } - + public static func placeholders() -> [Notification] { [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] } } - diff --git a/Packages/Models/Sources/Models/Poll.swift b/Packages/Models/Sources/Models/Poll.swift index eaba12b4..ccb97333 100644 --- a/Packages/Models/Sources/Models/Poll.swift +++ b/Packages/Models/Sources/Models/Poll.swift @@ -5,12 +5,12 @@ public struct Poll: Codable { enum CodingKeys: String, CodingKey { case title, votesCount } - + public var id = UUID().uuidString public let title: String public let votesCount: Int } - + public let id: String public let expiresAt: ServerDate public let expired: Bool diff --git a/Packages/Models/Sources/Models/PushSubscription.swift b/Packages/Models/Sources/Models/PushSubscription.swift index 62df6668..ecd3999c 100644 --- a/Packages/Models/Sources/Models/PushSubscription.swift +++ b/Packages/Models/Sources/Models/PushSubscription.swift @@ -9,7 +9,7 @@ public struct PushSubscription: Identifiable, Decodable { public let poll: Bool public let status: Bool } - + public let id: Int public let endpoint: URL public let serverKey: String diff --git a/Packages/Models/Sources/Models/Relationshionship.swift b/Packages/Models/Sources/Models/Relationshionship.swift index b8478eb6..0cb30db8 100644 --- a/Packages/Models/Sources/Models/Relationshionship.swift +++ b/Packages/Models/Sources/Models/Relationshionship.swift @@ -14,8 +14,8 @@ public struct Relationshionship: Codable { public let endorsed: Bool public let note: String public let notifying: Bool - - static public func placeholder() -> Relationshionship { + + public static func placeholder() -> Relationshionship { .init(id: UUID().uuidString, following: false, showingReblogs: false, diff --git a/Packages/Models/Sources/Models/SearchResults.swift b/Packages/Models/Sources/Models/SearchResults.swift index 8bb759da..a0cfa608 100644 --- a/Packages/Models/Sources/Models/SearchResults.swift +++ b/Packages/Models/Sources/Models/SearchResults.swift @@ -4,7 +4,7 @@ public struct SearchResults: Decodable { enum CodingKeys: String, CodingKey { case accounts, statuses, hashtags } - + public let accounts: [Account] public var relationships: [Relationshionship] = [] public let statuses: [Status] diff --git a/Packages/Models/Sources/Models/ServerPreferences.swift b/Packages/Models/Sources/Models/ServerPreferences.swift index 47915a47..541cd2f7 100644 --- a/Packages/Models/Sources/Models/ServerPreferences.swift +++ b/Packages/Models/Sources/Models/ServerPreferences.swift @@ -6,13 +6,13 @@ public struct ServerPreferences: Decodable { public let postLanguage: String? public let autoExpandmedia: AutoExpandMedia? public let autoExpandSpoilers: Bool? - + public enum AutoExpandMedia: String, Decodable { case showAll = "show_all" case hideAll = "hide_all" case hideSensitive = "default" } - + enum CodingKeys: String, CodingKey { case postVisibility = "posting:default:visibility" case postIsSensitive = "posting:default:sensitive" diff --git a/Packages/Models/Sources/Models/Status.swift b/Packages/Models/Sources/Models/Status.swift index 9ed8f466..2abe6e6e 100644 --- a/Packages/Models/Sources/Models/Status.swift +++ b/Packages/Models/Sources/Models/Status.swift @@ -4,12 +4,13 @@ public struct Application: Codable, Identifiable { public var id: String { name } + public let name: String public let website: URL? } -extension Application { - public init(from decoder: Decoder) throws { +public extension Application { + init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) name = try values.decodeIfPresent(String.self, forKey: .name) ?? "" @@ -53,12 +54,11 @@ public protocol AnyStatus { var language: String? { get } } - public struct Status: AnyStatus, Codable, Identifiable { public var viewId: String { id + createdAt + (editedAt ?? "") } - + public let id: String public let content: HTMLString public let account: Account @@ -85,7 +85,7 @@ public struct Status: AnyStatus, Codable, Identifiable { public let filtered: [Filtered]? public let sensitive: Bool public let language: String? - + public static func placeholder() -> Status { .init(id: UUID().uuidString, content: "This is a #toot\nWith some @content\nAnd some more content for your #eyes @only", @@ -114,7 +114,7 @@ public struct Status: AnyStatus, Codable, Identifiable { sensitive: false, language: nil) } - + public static func placeholders() -> [Status] { [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] } @@ -124,7 +124,7 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable { public var viewId: String { id + createdAt + (editedAt ?? "") } - + public let id: String public let content: String public let account: Account diff --git a/Packages/Models/Sources/Models/Stream/StreamEvent.swift b/Packages/Models/Sources/Models/Stream/StreamEvent.swift index e2a42cb3..1e067197 100644 --- a/Packages/Models/Sources/Models/Stream/StreamEvent.swift +++ b/Packages/Models/Sources/Models/Stream/StreamEvent.swift @@ -6,7 +6,7 @@ public struct RawStreamEvent: Decodable { public let payload: String } -public protocol StreamEvent: Identifiable{ +public protocol StreamEvent: Identifiable { var date: Date { get } var id: String { get } } diff --git a/Packages/Models/Sources/Models/Stream/StreamMessage.swift b/Packages/Models/Sources/Models/Stream/StreamMessage.swift index 71455154..c667987b 100644 --- a/Packages/Models/Sources/Models/Stream/StreamMessage.swift +++ b/Packages/Models/Sources/Models/Stream/StreamMessage.swift @@ -3,10 +3,9 @@ import Foundation public struct StreamMessage: Encodable { public let type: String public let stream: String - + public init(type: String, stream: String) { self.type = type self.stream = stream } - } diff --git a/Packages/Models/Sources/Models/Tag.swift b/Packages/Models/Sources/Models/Tag.swift index 45e7bb80..3db44e57 100644 --- a/Packages/Models/Sources/Models/Tag.swift +++ b/Packages/Models/Sources/Models/Tag.swift @@ -6,30 +6,30 @@ public struct Tag: Codable, Identifiable, Equatable, Hashable { public let accounts: String public let uses: String } - + public func hash(into hasher: inout Hasher) { hasher.combine(name) } - + public static func == (lhs: Tag, rhs: Tag) -> Bool { lhs.name == rhs.name } - + public var id: String { name } - + public let name: String public let url: String public let following: Bool public let history: [History] - + public var totalUses: Int { - history.compactMap{ Int($0.uses) }.reduce(0, +) + history.compactMap { Int($0.uses) }.reduce(0, +) } - + public var totalAccounts: Int { - history.compactMap{ Int($0.accounts) }.reduce(0, +) + history.compactMap { Int($0.accounts) }.reduce(0, +) } } @@ -41,11 +41,11 @@ public struct FeaturedTag: Codable, Identifiable { public var statusesCountInt: Int { Int(statusesCount) ?? 0 } - + private enum CodingKeys: String, CodingKey { case id, name, url, statusesCount } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) diff --git a/Packages/Models/Tests/ModelsTests/ModelsTests.swift b/Packages/Models/Tests/ModelsTests/ModelsTests.swift index 9397c1d2..11428a63 100644 --- a/Packages/Models/Tests/ModelsTests/ModelsTests.swift +++ b/Packages/Models/Tests/ModelsTests/ModelsTests.swift @@ -1,11 +1,11 @@ -import XCTest @testable import Models +import XCTest final class ModelsTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(Models().text, "Hello, World!") - } + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Models().text, "Hello, World!") + } } diff --git a/Packages/Network/Package.swift b/Packages/Network/Package.swift index ab5f1d2b..970dae84 100644 --- a/Packages/Network/Package.swift +++ b/Packages/Network/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "Network", - targets: ["Network"]), + targets: ["Network"] + ), ], dependencies: [ .package(name: "Models", path: "../Models"), @@ -20,10 +21,12 @@ let package = Package( .target( name: "Network", dependencies: [ - .product(name: "Models", package: "Models") - ]), + .product(name: "Models", package: "Models"), + ] + ), .testTarget( name: "NetworkTests", - dependencies: ["Network"]), + dependencies: ["Network"] + ), ] ) diff --git a/Packages/Network/Sources/Network/Client.swift b/Packages/Network/Sources/Network/Client.swift index 4812d488..805461dc 100644 --- a/Packages/Network/Sources/Network/Client.swift +++ b/Packages/Network/Sources/Network/Client.swift @@ -1,58 +1,58 @@ import Foundation -import SwiftUI import Models +import SwiftUI public class Client: ObservableObject, Equatable { public static func == (lhs: Client, rhs: Client) -> Bool { lhs.isAuth == rhs.isAuth && - lhs.server == rhs.server && - lhs.oauthToken?.accessToken == rhs.oauthToken?.accessToken + lhs.server == rhs.server && + lhs.oauthToken?.accessToken == rhs.oauthToken?.accessToken } - + public enum Version: String { case v1, v2 } - + public enum OauthError: Error { case missingApp case invalidRedirectURL } - + public var server: String public let version: Version public private(set) var connections: Set private let urlSession: URLSession private let decoder = JSONDecoder() - + /// Only used as a transitionary app while in the oauth flow. private var oauthApp: InstanceApp? - + private var oauthToken: OauthToken? - + public var isAuth: Bool { oauthToken != nil } - + public init(server: String, version: Version = .v1, oauthToken: OauthToken? = nil) { self.server = server self.version = version - self.urlSession = URLSession.shared - self.decoder.keyDecodingStrategy = .convertFromSnakeCase + urlSession = URLSession.shared + decoder.keyDecodingStrategy = .convertFromSnakeCase self.oauthToken = oauthToken - self.connections = Set([server]) + connections = Set([server]) } - + public func addConnections(_ connections: [String]) { connections.forEach { self.connections.insert($0) } } - + public func hasConnection(with url: URL) -> Bool { guard let host = url.host(percentEncoded: false) else { return false } return connections.contains(host) } - + private func makeURL(scheme: String = "https", endpoint: Endpoint, forceVersion: Version? = nil) -> URL { var components = URLComponents() components.scheme = scheme @@ -65,7 +65,7 @@ public class Client: ObservableObject, Equatable { components.queryItems = endpoint.queryItems() return components.url! } - + private func makeURLRequest(url: URL, endpoint: Endpoint, httpMethod: String) -> URLRequest { var request = URLRequest(url: url) request.httpMethod = httpMethod @@ -85,99 +85,103 @@ public class Client: ObservableObject, Equatable { } return request } - + private func makeGet(endpoint: Endpoint) -> URLRequest { let url = makeURL(endpoint: endpoint) return makeURLRequest(url: url, endpoint: endpoint, httpMethod: "GET") } - + 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?) { let (data, httpResponse) = try await urlSession.data(for: makeGet(endpoint: endpoint)) 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) } logResponseOnError(httpResponse: httpResponse, data: data) return (try decoder.decode(Entity.self, from: data), linkHandler) } - + public func post(endpoint: Endpoint) async throws -> Entity { try await makeEntityRequest(endpoint: endpoint, method: "POST") } - + public func post(endpoint: Endpoint) async throws -> HTTPURLResponse? { let url = makeURL(endpoint: endpoint) let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "POST") let (_, httpResponse) = try await urlSession.data(for: request) return httpResponse as? HTTPURLResponse } - + public func patch(endpoint: Endpoint) async throws -> HTTPURLResponse? { let url = makeURL(endpoint: endpoint) let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "PATCH") let (_, httpResponse) = try await urlSession.data(for: request) return httpResponse as? HTTPURLResponse } - + public func put(endpoint: Endpoint) async throws -> Entity { try await makeEntityRequest(endpoint: endpoint, method: "PUT") } - + public func delete(endpoint: Endpoint) async throws -> HTTPURLResponse? { let url = makeURL(endpoint: endpoint) 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 { + forceVersion: Version? = nil) async throws -> Entity + { let url = makeURL(endpoint: endpoint, forceVersion: forceVersion) let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) let (data, httpResponse) = try await urlSession.data(for: request) logResponseOnError(httpResponse: httpResponse, data: data) return try decoder.decode(Entity.self, from: data) } - + public func oauthURL() async throws -> URL { let app: InstanceApp = try await post(endpoint: Apps.registerApp) - self.oauthApp = app + oauthApp = app return makeURL(endpoint: Oauth.authorize(clientId: app.clientId)) } - + public func continueOauthFlow(url: URL) async throws -> OauthToken { guard let app = oauthApp else { throw OauthError.missingApp } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let code = components.queryItems?.first(where: { $0.name == "code"})?.value else { + 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)) - self.oauthToken = token + oauthToken = token return token } - + public func makeWebSocketTask(endpoint: Endpoint) -> URLSessionWebSocketTask { let url = makeURL(scheme: "wss", endpoint: endpoint) let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "GET") return urlSession.webSocketTask(with: request) } - + public func mediaUpload(endpoint: Endpoint, version: Version, method: String, mimeType: String, filename: String, - data: Data) async throws -> Entity { + data: Data) async throws -> Entity + { let url = makeURL(endpoint: endpoint, forceVersion: version) var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) let boundary = UUID().uuidString @@ -194,7 +198,7 @@ public class Client: ObservableObject, Equatable { logResponseOnError(httpResponse: httpResponse, data: data) return try decoder.decode(Entity.self, from: data) } - + private func logResponseOnError(httpResponse: URLResponse, data: Data) { if let httpResponse = httpResponse as? HTTPURLResponse, httpResponse.statusCode > 299 { print(httpResponse) diff --git a/Packages/Network/Sources/Network/Endpoint/Accounts.swift b/Packages/Network/Sources/Network/Endpoint/Accounts.swift index c04ef8a8..c723cf3e 100644 --- a/Packages/Network/Sources/Network/Endpoint/Accounts.swift +++ b/Packages/Network/Sources/Network/Endpoint/Accounts.swift @@ -30,10 +30,10 @@ public enum Accounts: Endpoint { case following(id: String, maxId: String?) case lists(id: String) case preferences - + public func path() -> String { switch self { - case .accounts(let id): + case let .accounts(id): return "accounts/\(id)" case .favourites: return "favourites" @@ -41,38 +41,38 @@ public enum Accounts: Endpoint { return "bookmarks" case .followedTags: return "followed_tags" - case .featuredTags(let id): + case let .featuredTags(id): return "accounts/\(id)/featured_tags" case .verifyCredentials: return "accounts/verify_credentials" case .updateCredentials: return "accounts/update_credentials" - case .statuses(let id, _, _, _, _, _): + case let .statuses(id, _, _, _, _, _): return "accounts/\(id)/statuses" case .relationships: return "accounts/relationships" - case .follow(let id, _): + case let .follow(id, _): return "accounts/\(id)/follow" - case .unfollow(let id): + case let .unfollow(id): return "accounts/\(id)/unfollow" case .familiarFollowers: return "accounts/familiar_followers" case .suggestions: return "suggestions" - case .following(let id, _): + case let .following(id, _): return "accounts/\(id)/following" - case .followers(let id, _): + case let .followers(id, _): return "accounts/\(id)/followers" - case .lists(let id): + case let .lists(id): return "accounts/\(id)/lists" case .preferences: return "preferences" } } - + public func queryItems() -> [URLQueryItem]? { switch self { - case .statuses(_, let sinceId, let tag, let onlyMedia, let excludeReplies, let pinned): + case let .statuses(_, sinceId, tag, onlyMedia, excludeReplies, pinned): var params: [URLQueryItem] = [] if let tag { params.append(.init(name: "tagged", value: tag)) diff --git a/Packages/Network/Sources/Network/Endpoint/Apps.swift b/Packages/Network/Sources/Network/Endpoint/Apps.swift index ed7ba9d1..ca2174ec 100644 --- a/Packages/Network/Sources/Network/Endpoint/Apps.swift +++ b/Packages/Network/Sources/Network/Endpoint/Apps.swift @@ -3,14 +3,14 @@ import Models public enum Apps: Endpoint { case registerApp - + public func path() -> String { switch self { case .registerApp: return "apps" } } - + public func queryItems() -> [URLQueryItem]? { switch self { case .registerApp: @@ -18,7 +18,7 @@ public enum Apps: Endpoint { .init(name: "client_name", value: AppInfo.clientName), .init(name: "redirect_uris", value: AppInfo.scheme), .init(name: "scopes", value: AppInfo.scopes), - .init(name: "website", value: AppInfo.weblink) + .init(name: "website", value: AppInfo.weblink), ] } } diff --git a/Packages/Network/Sources/Network/Endpoint/Conversations.swift b/Packages/Network/Sources/Network/Endpoint/Conversations.swift index bc7cc0c9..764f164b 100644 --- a/Packages/Network/Sources/Network/Endpoint/Conversations.swift +++ b/Packages/Network/Sources/Network/Endpoint/Conversations.swift @@ -4,7 +4,7 @@ public enum Conversations: Endpoint { case conversations case delete(id: String) case read(id: String) - + public func path() -> String { switch self { case .conversations: @@ -15,7 +15,7 @@ public enum Conversations: Endpoint { return "conversations/\(id)/read" } } - + public func queryItems() -> [URLQueryItem]? { return nil } diff --git a/Packages/Network/Sources/Network/Endpoint/Endpoint.swift b/Packages/Network/Sources/Network/Endpoint/Endpoint.swift index 97df2fd1..9bcd2b45 100644 --- a/Packages/Network/Sources/Network/Endpoint/Endpoint.swift +++ b/Packages/Network/Sources/Network/Endpoint/Endpoint.swift @@ -6,8 +6,8 @@ public protocol Endpoint { var jsonValue: Encodable? { get } } -extension Endpoint { - public var jsonValue: Encodable? { +public extension Endpoint { + var jsonValue: Encodable? { nil } } diff --git a/Packages/Network/Sources/Network/Endpoint/Instances.swift b/Packages/Network/Sources/Network/Endpoint/Instances.swift index 98ac8786..879b6147 100644 --- a/Packages/Network/Sources/Network/Endpoint/Instances.swift +++ b/Packages/Network/Sources/Network/Endpoint/Instances.swift @@ -3,7 +3,7 @@ import Foundation public enum Instances: Endpoint { case instance case peers - + public func path() -> String { switch self { case .instance: @@ -12,7 +12,7 @@ public enum Instances: Endpoint { return "instance/peers" } } - + public func queryItems() -> [URLQueryItem]? { nil } diff --git a/Packages/Network/Sources/Network/Endpoint/Lists.swift b/Packages/Network/Sources/Network/Endpoint/Lists.swift index 17e7c4d3..477d231e 100644 --- a/Packages/Network/Sources/Network/Endpoint/Lists.swift +++ b/Packages/Network/Sources/Network/Endpoint/Lists.swift @@ -6,7 +6,7 @@ public enum Lists: Endpoint { case createList(title: String) case accounts(listId: String) case updateAccounts(listId: String, accounts: [String]) - + public func path() -> String { switch self { case .lists, .createList: @@ -19,10 +19,10 @@ public enum Lists: Endpoint { return "lists/\(listId)/accounts" } } - + public func queryItems() -> [URLQueryItem]? { switch self { - case .accounts(_): + case .accounts: return [.init(name: "limit", value: String(0))] case let .createList(title): return [.init(name: "title", value: title)] diff --git a/Packages/Network/Sources/Network/Endpoint/Media.swift b/Packages/Network/Sources/Network/Endpoint/Media.swift index 086c5f5c..56204e6c 100644 --- a/Packages/Network/Sources/Network/Endpoint/Media.swift +++ b/Packages/Network/Sources/Network/Endpoint/Media.swift @@ -3,7 +3,7 @@ import Foundation public enum Media: Endpoint { case medias case media(id: String, description: String?) - + public func path() -> String { switch self { case .medias: @@ -12,7 +12,7 @@ public enum Media: Endpoint { return "media/\(id)" } } - + public func queryItems() -> [URLQueryItem]? { switch self { case let .media(_, description): diff --git a/Packages/Network/Sources/Network/Endpoint/Notifications.swift b/Packages/Network/Sources/Network/Endpoint/Notifications.swift index 45f96c03..3ffbc193 100644 --- a/Packages/Network/Sources/Network/Endpoint/Notifications.swift +++ b/Packages/Network/Sources/Network/Endpoint/Notifications.swift @@ -5,7 +5,7 @@ public enum Notifications: Endpoint { maxId: String?, types: [String]?) case clear - + public func path() -> String { switch self { case .notifications: @@ -14,10 +14,10 @@ public enum Notifications: Endpoint { return "notifications/clear" } } - + public func queryItems() -> [URLQueryItem]? { switch self { - case .notifications(let sinceId, let maxId, let types): + case let .notifications(sinceId, maxId, types): var params = makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: nil) ?? [] if let types { for type in types { diff --git a/Packages/Network/Sources/Network/Endpoint/Oauth.swift b/Packages/Network/Sources/Network/Endpoint/Oauth.swift index e368b2f5..f611f65e 100644 --- a/Packages/Network/Sources/Network/Endpoint/Oauth.swift +++ b/Packages/Network/Sources/Network/Endpoint/Oauth.swift @@ -4,7 +4,7 @@ import Models public enum Oauth: Endpoint { case authorize(clientId: String) case token(code: String, clientId: String, clientSecret: String) - + public func path() -> String { switch self { case .authorize: @@ -13,7 +13,7 @@ public enum Oauth: Endpoint { return "oauth/token" } } - + public func queryItems() -> [URLQueryItem]? { switch self { case let .authorize(clientId): @@ -21,7 +21,7 @@ public enum Oauth: Endpoint { .init(name: "response_type", value: "code"), .init(name: "client_id", value: clientId), .init(name: "redirect_uri", value: AppInfo.scheme), - .init(name: "scope", value: AppInfo.scopes) + .init(name: "scope", value: AppInfo.scopes), ] case let .token(code, clientId, clientSecret): return [ @@ -30,7 +30,7 @@ public enum Oauth: Endpoint { .init(name: "client_secret", value: clientSecret), .init(name: "redirect_uri", value: AppInfo.scheme), .init(name: "code", value: code), - .init(name: "scope", value: AppInfo.scopes) + .init(name: "scope", value: AppInfo.scopes), ] } } diff --git a/Packages/Network/Sources/Network/Endpoint/Polls.swift b/Packages/Network/Sources/Network/Endpoint/Polls.swift index 6add0ea5..d2411d68 100644 --- a/Packages/Network/Sources/Network/Endpoint/Polls.swift +++ b/Packages/Network/Sources/Network/Endpoint/Polls.swift @@ -3,16 +3,16 @@ import Foundation public enum Polls: Endpoint { case poll(id: String) case vote(id: String, votes: [Int]) - + public func path() -> String { switch self { - case .poll(let id): + case let .poll(id): return "polls/\(id)/" - case .vote(let id, _): + case let .vote(id, _): return "polls/\(id)/votes" } } - + public func queryItems() -> [URLQueryItem]? { switch self { case let .vote(_, votes): @@ -21,7 +21,7 @@ public enum Polls: Endpoint { params.append(.init(name: "choices[]", value: "\(vote)")) } return params - + default: return nil } diff --git a/Packages/Network/Sources/Network/Endpoint/Push.swift b/Packages/Network/Sources/Network/Endpoint/Push.swift index 2d6a6fd2..954e79a4 100644 --- a/Packages/Network/Sources/Network/Endpoint/Push.swift +++ b/Packages/Network/Sources/Network/Endpoint/Push.swift @@ -11,14 +11,14 @@ public enum Push: Endpoint { follow: Bool, favourite: Bool, poll: Bool) - + public func path() -> String { switch self { case .subscription, .createSub: return "push/subscription" } } - + public func queryItems() -> [URLQueryItem]? { switch self { case let .createSub(endpoint, p256dh, auth, mentions, status, reblog, follow, favourite, poll): diff --git a/Packages/Network/Sources/Network/Endpoint/Search.swift b/Packages/Network/Sources/Network/Endpoint/Search.swift index 2fc9784c..da70a9ff 100644 --- a/Packages/Network/Sources/Network/Endpoint/Search.swift +++ b/Packages/Network/Sources/Network/Endpoint/Search.swift @@ -2,14 +2,14 @@ import Foundation public enum Search: Endpoint { case search(query: String, type: String?, offset: Int?, following: Bool?) - + public func path() -> String { switch self { case .search: return "search" } } - + public func queryItems() -> [URLQueryItem]? { switch self { case let .search(query, type, offset, following): @@ -21,7 +21,7 @@ public enum Search: Endpoint { params.append(.init(name: "offset", value: String(offset))) } if let following { - params.append(.init(name: "following", value: following ? "true": "false")) + params.append(.init(name: "following", value: following ? "true" : "false")) } params.append(.init(name: "resolve", value: "true")) return params diff --git a/Packages/Network/Sources/Network/Endpoint/Statuses.swift b/Packages/Network/Sources/Network/Endpoint/Statuses.swift index 5a33576b..1b5c386e 100644 --- a/Packages/Network/Sources/Network/Endpoint/Statuses.swift +++ b/Packages/Network/Sources/Network/Endpoint/Statuses.swift @@ -16,28 +16,28 @@ public enum Statuses: Endpoint { case unpin(id: String) case bookmark(id: String) case unbookmark(id: String) - + public func path() -> String { switch self { case .postStatus: return "statuses" - case .status(let id): + case let .status(id): return "statuses/\(id)" - case .editStatus(let id, _): + case let .editStatus(id, _): return "statuses/\(id)" - case .context(let id): + case let .context(id): return "statuses/\(id)/context" - case .favourite(let id): + case let .favourite(id): return "statuses/\(id)/favourite" - case .unfavourite(let id): + case let .unfavourite(id): return "statuses/\(id)/unfavourite" - case .reblog(let id): + case let .reblog(id): return "statuses/\(id)/reblog" - case .unreblog(let id): + case let .unreblog(id): return "statuses/\(id)/unreblog" - case .rebloggedBy(let id, _): + case let .rebloggedBy(id, _): return "statuses/\(id)/reblogged_by" - case .favouritedBy(let id, _): + case let .favouritedBy(id, _): return "statuses/\(id)/favourited_by" case let .pin(id): return "statuses/\(id)/pin" @@ -49,7 +49,7 @@ public enum Statuses: Endpoint { return "statuses/\(id)/unbookmark" } } - + public func queryItems() -> [URLQueryItem]? { switch self { case let .rebloggedBy(_, maxId): @@ -60,7 +60,7 @@ public enum Statuses: Endpoint { return nil } } - + public var jsonValue: Encodable? { switch self { case let .postStatus(json): @@ -86,21 +86,22 @@ public struct StatusData: Encodable { public let options: [String] public let multiple: Bool public let expires_in: Int - + public init(options: [String], multiple: Bool, expires_in: Int) { self.options = options self.multiple = multiple self.expires_in = expires_in } } - + public init(status: String, visibility: Visibility, inReplyToId: String? = nil, spoilerText: String? = nil, mediaIds: [String]? = nil, poll: PollData? = nil, - language: String? = nil) { + language: String? = nil) + { self.status = status self.visibility = visibility self.inReplyToId = inReplyToId diff --git a/Packages/Network/Sources/Network/Endpoint/Streaming.swift b/Packages/Network/Sources/Network/Endpoint/Streaming.swift index 25feabb6..791d3407 100644 --- a/Packages/Network/Sources/Network/Endpoint/Streaming.swift +++ b/Packages/Network/Sources/Network/Endpoint/Streaming.swift @@ -2,14 +2,14 @@ import Foundation public enum Streaming: Endpoint { case streaming - + public func path() -> String { switch self { case .streaming: return "streaming" } } - + public func queryItems() -> [URLQueryItem]? { switch self { default: diff --git a/Packages/Network/Sources/Network/Endpoint/Tags.swift b/Packages/Network/Sources/Network/Endpoint/Tags.swift index 8d75e225..4e9d2816 100644 --- a/Packages/Network/Sources/Network/Endpoint/Tags.swift +++ b/Packages/Network/Sources/Network/Endpoint/Tags.swift @@ -4,18 +4,18 @@ public enum Tags: Endpoint { case tag(id: String) case follow(id: String) case unfollow(id: String) - + public func path() -> String { switch self { - case .tag(let id): + case let .tag(id): return "tags/\(id)/" - case .follow(let id): + case let .follow(id): return "tags/\(id)/follow" - case .unfollow(let id): + case let .unfollow(id): return "tags/\(id)/unfollow" } } - + public func queryItems() -> [URLQueryItem]? { switch self { default: diff --git a/Packages/Network/Sources/Network/Endpoint/Timelines.swift b/Packages/Network/Sources/Network/Endpoint/Timelines.swift index 13bbe548..59c5e467 100644 --- a/Packages/Network/Sources/Network/Endpoint/Timelines.swift +++ b/Packages/Network/Sources/Network/Endpoint/Timelines.swift @@ -5,7 +5,7 @@ public enum Timelines: Endpoint { case home(sinceId: String?, maxId: String?, minId: String?) case list(listId: String, sinceId: String?, maxId: String?, minId: String?) case hashtag(tag: String, maxId: String?) - + public func path() -> String { switch self { case .pub: @@ -18,16 +18,16 @@ public enum Timelines: Endpoint { return "timelines/tag/\(tag)" } } - + public func queryItems() -> [URLQueryItem]? { switch self { - case .pub(let sinceId, let maxId, let minId, let local): + case let .pub(sinceId, maxId, minId, local): var params = makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: minId) ?? [] params.append(.init(name: "local", value: local ? "true" : "false")) return params - case .home(let sinceId, let maxId, let mindId): + case let .home(sinceId, maxId, mindId): return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId) - case .list(_, let sinceId, let maxId, let mindId): + case let .list(_, sinceId, maxId, mindId): return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId) case let .hashtag(_, maxId): return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) diff --git a/Packages/Network/Sources/Network/Endpoint/Trends.swift b/Packages/Network/Sources/Network/Endpoint/Trends.swift index 16bb3933..43e3e6bb 100644 --- a/Packages/Network/Sources/Network/Endpoint/Trends.swift +++ b/Packages/Network/Sources/Network/Endpoint/Trends.swift @@ -4,7 +4,7 @@ public enum Trends: Endpoint { case tags case statuses(offset: Int?) case links - + public func path() -> String { switch self { case .tags: @@ -15,7 +15,7 @@ public enum Trends: Endpoint { return "trends/links" } } - + public func queryItems() -> [URLQueryItem]? { switch self { case let .statuses(offset): diff --git a/Packages/Network/Sources/Network/InstanceSocialClient.swift b/Packages/Network/Sources/Network/InstanceSocialClient.swift index 7f91ea66..5606b4cd 100644 --- a/Packages/Network/Sources/Network/InstanceSocialClient.swift +++ b/Packages/Network/Sources/Network/InstanceSocialClient.swift @@ -4,15 +4,13 @@ import Models public struct InstanceSocialClient { private let authorization = "Bearer 8a4xx3D7Hzu1aFnf18qlkH8oU0oZ5ulabXxoS2FtQtwOy8G0DGQhr5PjTIjBnYAmFrSBuE2CcASjFocxJBonY8XGbLySB7MXd9ssrwlRHUXTQh3Z578lE1OfUtafvhML" private let endpoint = URL(string: "https://instances.social/api/1.0/instances/list?count=1000&include_closed=false&include_dead=false&min_active_users=500")! - + struct Response: Decodable { let instances: [InstanceSocial] } - - public init() { - - } - + + public init() {} + public func fetchInstances() async -> [InstanceSocial] { do { let decoder = JSONDecoder() diff --git a/Packages/Network/Sources/Network/LinkHandler.swift b/Packages/Network/Sources/Network/LinkHandler.swift index 73dcf736..ef6e5d06 100644 --- a/Packages/Network/Sources/Network/LinkHandler.swift +++ b/Packages/Network/Sources/Network/LinkHandler.swift @@ -3,7 +3,7 @@ import RegexBuilder public struct LinkHandler { public let rawLink: String - + public var maxId: String? { do { let regex = try Regex("max_id=[0-9]+") diff --git a/Packages/Network/Sources/Network/OpenAIClient.swift b/Packages/Network/Sources/Network/OpenAIClient.swift index 468be874..cf31222b 100644 --- a/Packages/Network/Sources/Network/OpenAIClient.swift +++ b/Packages/Network/Sources/Network/OpenAIClient.swift @@ -1,8 +1,8 @@ import Foundation public struct OpenAIClient { - private let endpoint: URL = URL(string: "https://api.openai.com/v1/completions")! - + private let endpoint: URL = .init(string: "https://api.openai.com/v1/completions")! + private var APIKey: String { if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") { let secret = NSDictionary(contentsOfFile: path) @@ -10,23 +10,23 @@ public struct OpenAIClient { } return "" } - + private var authorizationHeaderValue: String { "Bearer \(APIKey)" } - + private var encoder: JSONEncoder { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase return encoder } - + private var decoder: JSONDecoder { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase return decoder } - + public struct Request: Encodable { let model = "text-davinci-003" let topP: Int = 1 @@ -35,19 +35,19 @@ public struct OpenAIClient { let prompt: String let temperature: Double let maxTokens: Int - + public init(prompt: String, temperature: Double, maxTokens: Int) { self.prompt = prompt self.temperature = temperature self.maxTokens = maxTokens } } - + public enum Prompts { case correct(input: String) case shorten(input: String) case emphasize(input: String) - + var request: Request { switch self { case let .correct(input): @@ -65,20 +65,20 @@ public struct OpenAIClient { } } } - + public struct Response: Decodable { public struct Choice: Decodable { public let text: String } - + public let id: String public let object: String public let model: String public let choices: [Choice] } - - public init() { } - + + public init() {} + public func request(_ prompt: Prompts) async throws -> Response { do { let jsonData = try encoder.encode(prompt.request) @@ -90,7 +90,7 @@ public struct OpenAIClient { let (result, _) = try await URLSession.shared.data(for: request) let response = try decoder.decode(Response.self, from: result) return response - } catch let error { + } catch { throw error } } diff --git a/Packages/Network/Tests/NetworkTests/NetworkTests.swift b/Packages/Network/Tests/NetworkTests/NetworkTests.swift index bcd03cac..eb6dc679 100644 --- a/Packages/Network/Tests/NetworkTests/NetworkTests.swift +++ b/Packages/Network/Tests/NetworkTests/NetworkTests.swift @@ -1,11 +1,11 @@ -import XCTest @testable import Network +import XCTest final class NetworkTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(Network().text, "Hello, World!") - } + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Network().text, "Hello, World!") + } } diff --git a/Packages/Notifications/Package.swift b/Packages/Notifications/Package.swift index c626ec86..3bc1940e 100644 --- a/Packages/Notifications/Package.swift +++ b/Packages/Notifications/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "Notifications", - targets: ["Notifications"]), + targets: ["Notifications"] + ), ], dependencies: [ .package(name: "Network", path: "../Network"), @@ -28,8 +29,8 @@ let package = Package( .product(name: "Models", package: "Models"), .product(name: "Env", package: "Env"), .product(name: "Status", package: "Status"), - .product(name: "DesignSystem", package: "DesignSystem") - ]), + .product(name: "DesignSystem", package: "DesignSystem"), + ] + ), ] ) - diff --git a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift index 49de7a3b..1622fefc 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift @@ -1,17 +1,17 @@ -import SwiftUI -import Models import DesignSystem -import Status -import Env import EmojiText +import Env +import Models +import Status +import SwiftUI struct NotificationRowView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var routeurPath: RouterPath @Environment(\.redactionReasons) private var reasons - + let notification: Models.Notification - + var body: some View { if let type = notification.supportedType { HStack(alignment: .top, spacing: 8) { @@ -25,7 +25,7 @@ struct NotificationRowView: View { EmptyView() } } - + private func makeAvatarView(type: Models.Notification.NotificationType) -> some View { ZStack(alignment: .topLeading) { AvatarView(url: notification.account.avatar) @@ -34,7 +34,7 @@ struct NotificationRowView: View { .strokeBorder(Color.white, lineWidth: 1) .background(Circle().foregroundColor(theme.tintColor)) .frame(width: 24, height: 24) - + Image(systemName: type.iconName()) .resizable() .frame(width: 12, height: 12) @@ -47,28 +47,28 @@ struct NotificationRowView: View { routeurPath.navigate(to: .accountDetailWithAccount(account: notification.account)) } } - + private func makeMainLabel(type: Models.Notification.NotificationType) -> some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 0) { EmojiTextApp(notification.account.safeDisplayName.asMarkdown, emojis: notification.account.emojis, append: { - Text(" ") + - Text(type.label()) - .font(.subheadline) - .fontWeight(.regular) + - Text(" ⸱ ") - .font(.footnote) - .fontWeight(.regular) - .foregroundColor(.gray) + - Text(notification.createdAt.formatted) - .font(.footnote) - .fontWeight(.regular) - .foregroundColor(.gray) - }) - .font(.subheadline) - .fontWeight(.semibold) + Text(" ") + + Text(type.label()) + .font(.subheadline) + .fontWeight(.regular) + + Text(" ⸱ ") + .font(.footnote) + .fontWeight(.regular) + .foregroundColor(.gray) + + Text(notification.createdAt.formatted) + .font(.footnote) + .fontWeight(.regular) + .foregroundColor(.gray) + }) + .font(.subheadline) + .fontWeight(.semibold) Spacer() } } @@ -77,7 +77,7 @@ struct NotificationRowView: View { routeurPath.navigate(to: .accountDetailWithAccount(account: notification.account)) } } - + @ViewBuilder private func makeContent(type: Models.Notification.NotificationType) -> some View { if let status = notification.status { @@ -95,7 +95,7 @@ struct NotificationRowView: View { Text("@\(notification.account.acct)") .font(.callout) .foregroundColor(.gray) - + if type == .follow { EmojiTextApp(notification.account.note.asMarkdown, emojis: notification.account.emojis) diff --git a/Packages/Notifications/Sources/Notifications/NotificationTypeExt.swift b/Packages/Notifications/Sources/Notifications/NotificationTypeExt.swift index 548324b1..51b79e9e 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationTypeExt.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationTypeExt.swift @@ -21,7 +21,7 @@ extension Models.Notification.NotificationType { return "edited a post" } } - + func iconName() -> String { switch self { case .status: @@ -40,7 +40,7 @@ extension Models.Notification.NotificationType { return "pencil.line" } } - + func menuTitle() -> String { switch self { case .status: diff --git a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift index fa619884..b05c51d6 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift @@ -1,9 +1,9 @@ -import SwiftUI -import Network -import Models -import Shimmer import DesignSystem import Env +import Models +import Network +import Shimmer +import SwiftUI public struct NotificationsListView: View { @Environment(\.scenePhase) private var scenePhase @@ -11,9 +11,9 @@ public struct NotificationsListView: View { @EnvironmentObject private var watcher: StreamWatcher @EnvironmentObject private var client: Client @StateObject private var viewModel = NotificationsViewModel() - - public init() { } - + + public init() {} + public var body: some View { ScrollView { LazyVStack { @@ -71,7 +71,7 @@ public struct NotificationsListView: View { } }) } - + @ViewBuilder private var notificationsView: some View { switch viewModel.state { @@ -83,7 +83,7 @@ public struct NotificationsListView: View { Divider() .padding(.vertical, .dividerPadding) } - + case let .display(notifications, nextPageState): if notifications.isEmpty { EmptyView(iconName: "bell.slash", @@ -91,14 +91,14 @@ public struct NotificationsListView: View { message: "Notifications? What notifications? Your notification inbox is looking so empty. Keep on being awesome! 📱😎") } else { ForEach(notifications) { notification in - if notification.supportedType != nil { - NotificationRowView(notification: notification) - Divider() - .padding(.vertical, .dividerPadding) - } + if notification.supportedType != nil { + NotificationRowView(notification: notification) + Divider() + .padding(.vertical, .dividerPadding) + } } } - + switch nextPageState { case .none: EmptyView() @@ -112,7 +112,7 @@ public struct NotificationsListView: View { case .loadingNextPage: loadingRow } - + case .error: ErrorView(title: "An error occured", message: "An error occured while loading your notifications, please retry.", @@ -123,7 +123,7 @@ public struct NotificationsListView: View { } } } - + private var loadingRow: some View { HStack { Spacer() diff --git a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift index 07ac1117..7490fb56 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift @@ -1,7 +1,7 @@ import Foundation -import SwiftUI -import Network import Models +import Network +import SwiftUI @MainActor class NotificationsViewModel: ObservableObject { @@ -9,16 +9,17 @@ class NotificationsViewModel: ObservableObject { public enum PagingState { case none, hasNextPage, loadingNextPage } + case loading case display(notifications: [Models.Notification], nextPageState: State.PagingState) case error(error: Error) } - + public enum Tab: String, CaseIterable { case all = "All" case mentions = "Mentions" } - + var client: Client? { didSet { if oldValue != client { @@ -26,6 +27,7 @@ class NotificationsViewModel: ObservableObject { } } } + @Published var state: State = .loading @Published var selectedType: Models.Notification.NotificationType? { didSet { @@ -35,12 +37,13 @@ class NotificationsViewModel: ObservableObject { } } } + private var queryTypes: [String]? { selectedType != nil ? [selectedType!.rawValue] : nil - } - + } + private var notifications: [Models.Notification] = [] - + func fetchNotifications() async { guard let client else { return } do { @@ -53,13 +56,13 @@ class NotificationsViewModel: ObservableObject { nextPageState = notifications.count < 15 ? .none : .hasNextPage } else if let first = notifications.first { var newNotifications: [Models.Notification] = - try await client.get(endpoint: Notifications.notifications(sinceId: first.id, - maxId: nil, - types: queryTypes)) + try await client.get(endpoint: Notifications.notifications(sinceId: first.id, + maxId: nil, + types: queryTypes)) nextPageState = notifications.count < 15 ? .none : .hasNextPage - newNotifications = newNotifications.filter({ notification in + newNotifications = newNotifications.filter { notification in !notifications.contains(where: { $0.id == notification.id }) - }) + } notifications.insert(contentsOf: newNotifications, at: 0) } withAnimation { @@ -70,33 +73,34 @@ class NotificationsViewModel: ObservableObject { state = .error(error: error) } } - + func fetchNextPage() async { guard let client else { return } do { guard let lastId = notifications.last?.id else { return } state = .display(notifications: notifications, nextPageState: .loadingNextPage) let newNotifications: [Models.Notification] = - try await client.get(endpoint: Notifications.notifications(sinceId: nil, - maxId: lastId, - types: queryTypes)) + try await client.get(endpoint: Notifications.notifications(sinceId: nil, + maxId: lastId, + types: queryTypes)) notifications.append(contentsOf: newNotifications) state = .display(notifications: notifications, nextPageState: newNotifications.count < 15 ? .none : .hasNextPage) } catch { state = .error(error: error) } } - + func clear() async { guard let client else { return } do { let _: ServerError = try await client.post(endpoint: Notifications.clear) - } catch { } + } catch {} } - + func handleEvent(event: any StreamEvent) { if let event = event as? StreamEventNotification, - !notifications.contains(where: { $0.id == event.notification.id }) { + !notifications.contains(where: { $0.id == event.notification.id }) + { notifications.insert(event.notification, at: 0) state = .display(notifications: notifications, nextPageState: .hasNextPage) } diff --git a/Packages/Status/Package.swift b/Packages/Status/Package.swift index a21cfcc1..1eb27081 100644 --- a/Packages/Status/Package.swift +++ b/Packages/Status/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "Status", - targets: ["Status"]), + targets: ["Status"] + ), ], dependencies: [ .package(name: "AppAccount", path: "../AppAccount"), @@ -31,7 +32,7 @@ let package = Package( .product(name: "Env", package: "Env"), .product(name: "DesignSystem", package: "DesignSystem"), .product(name: "TextView", package: "TextView"), - ]), + ] + ), ] ) - diff --git a/Packages/Status/Sources/Status/Detail/StatusDetailVIew.swift b/Packages/Status/Sources/Status/Detail/StatusDetailVIew.swift index 17ed3638..74e56520 100644 --- a/Packages/Status/Sources/Status/Detail/StatusDetailVIew.swift +++ b/Packages/Status/Sources/Status/Detail/StatusDetailVIew.swift @@ -1,9 +1,9 @@ -import SwiftUI -import Models -import Shimmer -import Env -import Network import DesignSystem +import Env +import Models +import Network +import Shimmer +import SwiftUI public struct StatusDetailView: View { @EnvironmentObject private var theme: Theme @@ -14,15 +14,15 @@ public struct StatusDetailView: View { @Environment(\.openURL) private var openURL @StateObject private var viewModel: StatusDetailViewModel @State private var isLoaded: Bool = false - + public init(statusId: String) { _viewModel = StateObject(wrappedValue: .init(statusId: statusId)) } - + public init(remoteStatusURL: URL) { _viewModel = StateObject(wrappedValue: .init(remoteStatusURL: remoteStatusURL)) } - + public var body: some View { ScrollViewReader { proxy in ScrollView { @@ -37,7 +37,7 @@ public struct StatusDetailView: View { Divider() .padding(.vertical, .dividerPadding) } - case let.display(status, context): + case let .display(status, context): if !context.ancestors.isEmpty { ForEach(context.ancestors) { ancestor in StatusRowView(viewModel: .init(status: ancestor, isCompact: false)) @@ -49,7 +49,7 @@ public struct StatusDetailView: View { StatusRowView(viewModel: .init(status: status, isCompact: false, isFocused: true)) - .padding(.horizontal, .layoutPadding) + .padding(.horizontal, .layoutPadding) .id(status.id) Divider() .padding(.bottom, .dividerPadding * 2) @@ -61,7 +61,7 @@ public struct StatusDetailView: View { .padding(.vertical, .dividerPadding) } } - + case .error: ErrorView(title: "An error occured", message: "An error occured while this post context, please try again.", diff --git a/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift b/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift index c5134d02..542d75a6 100644 --- a/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift +++ b/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift @@ -1,34 +1,34 @@ import Foundation -import SwiftUI import Models import Network +import SwiftUI @MainActor class StatusDetailViewModel: ObservableObject { public var statusId: String? public var remoteStatusURL: URL? - + var client: Client? - + enum State { case loading, display(status: Status, context: StatusContext), error(error: Error) } - + @Published var state: State = .loading @Published var title: String = "" - + init(statusId: String) { state = .loading self.statusId = statusId - self.remoteStatusURL = nil + remoteStatusURL = nil } - + init(remoteStatusURL: URL) { state = .loading self.remoteStatusURL = remoteStatusURL - self.statusId = nil + statusId = nil } - + func fetch() async -> Bool { if statusId != nil { await fetchStatusDetail() @@ -38,11 +38,11 @@ class StatusDetailViewModel: ObservableObject { } return false } - + 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", + type: "statuses", offset: nil, following: nil), forceVersion: .v2) @@ -54,7 +54,7 @@ class StatusDetailViewModel: ObservableObject { return false } } - + private func fetchStatusDetail() async { guard let client, let statusId else { return } do { @@ -66,11 +66,11 @@ class StatusDetailViewModel: ObservableObject { state = .error(error: error) } } - - + func handleEvent(event: any StreamEvent, currentAccount: Account?) { if let event = event as? StreamEventUpdate, - event.status.account.id == currentAccount?.id { + event.status.account.id == currentAccount?.id + { Task { await fetchStatusDetail() } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAIPrompts.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAIPrompts.swift index 6af189eb..ee74c814 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAIPrompts.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAIPrompts.swift @@ -1,10 +1,10 @@ import Foundation -import SwiftUI import Network +import SwiftUI enum StatusEditorAIPrompts: CaseIterable { case correct, fit, emphasize - + @ViewBuilder var label: some View { switch self { @@ -16,7 +16,7 @@ enum StatusEditorAIPrompts: CaseIterable { Label("Emphasize text", systemImage: "text.badge.star") } } - + func toRequestPrompt(text: String) -> OpenAIClient.Prompts { switch self { case .correct: diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift index 94595cb4..4ec805cd 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift @@ -1,21 +1,21 @@ -import SwiftUI import DesignSystem -import PhotosUI -import Models import Env +import Models +import PhotosUI +import SwiftUI struct StatusEditorAccessoryView: View { @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var theme: Theme @EnvironmentObject private var currentInstance: CurrentInstance - + @FocusState.Binding var isSpoilerTextFocused: Bool @ObservedObject var viewModel: StatusEditorViewModel - + @State private var isDrafsSheetDisplayed: Bool = false @State private var isLanguageSheetDisplayed: Bool = false @State private var languageSearch: String = "" - + var body: some View { VStack(spacing: 0) { Divider() @@ -25,7 +25,7 @@ struct StatusEditorAccessoryView: View { Image(systemName: "photo.fill.on.rectangle.fill") } .disabled(viewModel.showPoll) - + Button { withAnimation { viewModel.showPoll.toggle() @@ -34,16 +34,16 @@ struct StatusEditorAccessoryView: View { Image(systemName: "chart.bar") } .disabled(viewModel.shouldDisablePollButton) - + Button { withAnimation { viewModel.spoilerOn.toggle() } isSpoilerTextFocused.toggle() } label: { - Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle") + Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill" : "exclamationmark.triangle") } - + if !viewModel.mode.isInShareExtension { Button { isDrafsSheetDisplayed = true @@ -61,9 +61,9 @@ struct StatusEditorAccessoryView: View { Image(systemName: "globe") } } - + Spacer() - + characterCountView } .frame(height: 20) @@ -81,7 +81,7 @@ struct StatusEditorAccessoryView: View { viewModel.setInitialLanguageSelection(preference: preferences.serverPreferences?.postLanguage) } } - + @ViewBuilder private func languageTextView(isoCode: String, nativeName: String?, name: String?) -> some View { if let nativeName = nativeName, let name = name { @@ -90,11 +90,11 @@ struct StatusEditorAccessoryView: View { Text(isoCode.uppercased()) } } - + private var languageSheetView: some View { NavigationStack { List { - ForEach(availableLanguages, id: \.0) { (isoCode, nativeName, name) in + ForEach(availableLanguages, id: \.0) { isoCode, nativeName, name in HStack { languageTextView(isoCode: isoCode, nativeName: nativeName, name: name) .tag(isoCode) @@ -123,7 +123,7 @@ struct StatusEditorAccessoryView: View { .background(theme.secondaryBackgroundColor) } } - + private var draftsSheetView: some View { NavigationStack { List { @@ -154,14 +154,13 @@ struct StatusEditorAccessoryView: View { } .presentationDetents([.medium]) } - - + private var characterCountView: some View { Text("\((currentInstance.instance?.configuration.statuses.maxCharacters ?? 500) - viewModel.statusText.string.utf16.count)") .foregroundColor(.gray) .font(.callout) } - + private var availableLanguages: [(String, String?, String?)] { Locale.LanguageCode.isoLanguageCodes .filter { $0.identifier.count == 2 } // Mastodon only supports ISO 639-1 (two-letter) codes @@ -173,7 +172,7 @@ struct StatusEditorAccessoryView: View { Locale.current.localizedString(forLanguageCode: lang.identifier) ) } - .filter { (identifier, nativeLocale, locale) in + .filter { _, nativeLocale, _ in guard !languageSearch.isEmpty else { return true } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift index 286c7a92..155bcd6a 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift @@ -1,12 +1,12 @@ -import Foundation -import SwiftUI import DesignSystem import EmojiText +import Foundation +import SwiftUI struct StatusEditorAutoCompleteView: View { @EnvironmentObject private var theme: Theme @ObservedObject var viewModel: StatusEditorViewModel - + var body: some View { if !viewModel.mentionsSuggestions.isEmpty || !viewModel.tagsSuggestions.isEmpty { ScrollView(.horizontal, showsIndicators: false) { @@ -23,7 +23,7 @@ struct StatusEditorAutoCompleteView: View { .background(.ultraThinMaterial) } } - + private var suggestionsMentionsView: some View { ForEach(viewModel.mentionsSuggestions) { account in Button { @@ -56,5 +56,4 @@ struct StatusEditorAutoCompleteView: View { } } } - } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift index 28fe9ce3..9744465f 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift @@ -1,16 +1,16 @@ -import SwiftUI -import Models import DesignSystem +import Models import Shimmer +import SwiftUI struct StatusEditorMediaEditView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var theme: Theme @ObservedObject var viewModel: StatusEditorViewModel let container: StatusEditorViewModel.ImageContainer - + @State private var imageDescription: String = "" - + var body: some View { NavigationStack { Form { @@ -34,7 +34,8 @@ struct StatusEditorMediaEditView: View { .fill(Color.gray) .frame(height: 200) .shimmering() - }) + } + ) } } .listRowBackground(theme.primaryBackgroundColor) @@ -57,7 +58,7 @@ struct StatusEditorMediaEditView: View { dismiss() } } - + ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift index 2cab5a01..aaf08a61 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift @@ -1,14 +1,14 @@ -import SwiftUI +import DesignSystem import Env import Models -import DesignSystem import NukeUI +import SwiftUI struct StatusEditorMediaView: View { @EnvironmentObject private var theme: Theme @ObservedObject var viewModel: StatusEditorViewModel @State private var editingContainer: StatusEditorViewModel.ImageContainer? - + var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { @@ -19,7 +19,7 @@ struct StatusEditorMediaView: View { ZStack(alignment: .bottomTrailing) { if container.image != nil { makeLocalImage(container: container) - } else if let url = container.mediaAttachement?.url ?? container.mediaAttachement?.previewUrl { + } else if let url = container.mediaAttachement?.url ?? container.mediaAttachement?.previewUrl { makeLazyImage(url: url) } if container.mediaAttachement?.description?.isEmpty == false { @@ -36,7 +36,7 @@ struct StatusEditorMediaView: View { .preferredColorScheme(theme.selectedScheme == .dark ? .dark : .light) } } - + private func makeLocalImage(container: StatusEditorViewModel.ImageContainer) -> some View { ZStack(alignment: .center) { Image(uiImage: container.image!) @@ -69,12 +69,12 @@ struct StatusEditorMediaView: View { } .buttonStyle(.bordered) } - } else if container.mediaAttachement == nil{ + } else if container.mediaAttachement == nil { ProgressView() } } } - + private func makeLazyImage(url: URL?) -> some View { LazyImage(url: url) { state in if let image = state.image { @@ -89,7 +89,7 @@ struct StatusEditorMediaView: View { .frame(width: 150, height: 150) .cornerRadius(8) } - + @ViewBuilder private func makeImageMenu(container: StatusEditorViewModel.ImageContainer) -> some View { if !viewModel.mode.isEditing { @@ -97,8 +97,8 @@ struct StatusEditorMediaView: View { editingContainer = container } label: { Label(container.mediaAttachement?.description?.isEmpty == false ? - "Edit description" : "Add description", - systemImage: "pencil.line") + "Edit description" : "Add description", + systemImage: "pencil.line") } } Button(role: .destructive) { @@ -109,10 +109,9 @@ struct StatusEditorMediaView: View { Label("Delete", systemImage: "trash") } } - + private var altMarker: some View { - Button { - } label: { + Button {} label: { Text("ALT") .font(.caption2) } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift index 13a2c826..e5802b07 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift @@ -1,6 +1,6 @@ -import SwiftUI import DesignSystem import Env +import SwiftUI struct StatusEditorPollView: View { enum FocusField: Hashable { @@ -22,7 +22,7 @@ struct StatusEditorPollView: View { let count = viewModel.pollOptions.count VStack { - ForEach(0.. Any? { let result = try await item.loadItem(forTypeIdentifier: rawValue) if self == .jpeg || self == .png, - let imageURL = result as? URL, - let data = try? Data(contentsOf: imageURL), - let image = UIImage(data: data) { - return image + let imageURL = result as? URL, + let data = try? Data(contentsOf: imageURL), + let image = UIImage(data: data) + { + return image } if let url = result as? URL { return url.absoluteString diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index 7b4e386d..4c97017d 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -1,15 +1,15 @@ -import SwiftUI import Accounts -import Env +import AppAccount import DesignSystem -import TextView +import EmojiText +import Env import Models import Network -import PhotosUI import NukeUI -import EmojiText +import PhotosUI +import SwiftUI +import TextView import UIKit -import AppAccount public struct StatusEditorView: View { @EnvironmentObject private var preferences: UserPreferences @@ -17,17 +17,17 @@ public struct StatusEditorView: View { @EnvironmentObject private var client: Client @EnvironmentObject private var currentAccount: CurrentAccount @Environment(\.dismiss) private var dismiss - + @StateObject private var viewModel: StatusEditorViewModel @FocusState private var isSpoilerTextFocused: Bool - + @State private var isDismissAlertPresented: Bool = false @State private var isLoadingAIRequest: Bool = false - + public init(mode: StatusEditorViewModel.Mode) { _viewModel = StateObject(wrappedValue: .init(mode: mode)) } - + public var body: some View { NavigationStack { ZStack(alignment: .bottom) { @@ -120,25 +120,25 @@ public struct StatusEditorView: View { .confirmationDialog("", isPresented: $isDismissAlertPresented, actions: { - Button("Delete Draft", role: .destructive) { - dismiss() - NotificationCenter.default.post(name: NotificationsName.shareSheetClose, - object: nil) - } - Button("Save Draft") { - preferences.draftsPosts.insert(viewModel.statusText.string, at: 0) - dismiss() - NotificationCenter.default.post(name: NotificationsName.shareSheetClose, - object: nil) - } - Button("Cancel", role: .cancel) { } - }) + Button("Delete Draft", role: .destructive) { + dismiss() + NotificationCenter.default.post(name: NotificationsName.shareSheetClose, + object: nil) + } + Button("Save Draft") { + preferences.draftsPosts.insert(viewModel.statusText.string, at: 0) + dismiss() + NotificationCenter.default.post(name: NotificationsName.shareSheetClose, + object: nil) + } + Button("Cancel", role: .cancel) {} + }) } } } .interactiveDismissDisabled(!viewModel.statusText.string.isEmpty) } - + @ViewBuilder private var spoilerTextView: some View { if viewModel.spoilerOn { @@ -152,7 +152,7 @@ public struct StatusEditorView: View { .offset(y: -8) } } - + @ViewBuilder private var accountHeaderView: some View { if let account = currentAccount.account { @@ -170,7 +170,7 @@ public struct StatusEditorView: View { } } } - + private var privacyMenu: some View { Menu { Section("Post visibility") { @@ -195,7 +195,7 @@ public struct StatusEditorView: View { ) } } - + private var AIMenu: some View { Menu { ForEach(StatusEditorAIPrompts.allCases, id: \.self) { prompt in @@ -224,6 +224,5 @@ public struct StatusEditorView: View { Image(systemName: "faxmachine") } } - } } diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index 1ac4b356..fb9b3c50 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -1,9 +1,9 @@ -import SwiftUI import DesignSystem import Env import Models import Network import PhotosUI +import SwiftUI @MainActor public class StatusEditorViewModel: ObservableObject { @@ -13,20 +13,21 @@ public class StatusEditorViewModel: ObservableObject { let mediaAttachement: MediaAttachement? let error: Error? } - + var mode: Mode let generator = UINotificationFeedbackGenerator() - + var client: Client? var currentAccount: Account? var theme: Theme? - + @Published var statusText = NSMutableAttributedString(string: "") { didSet { processText() checkEmbed() } } + @Published var backupStatustext: NSAttributedString? @Published var showPoll: Bool = false @@ -36,18 +37,19 @@ public class StatusEditorViewModel: ObservableObject { @Published var spoilerOn: Bool = false @Published var spoilerText: String = "" - + @Published var selectedRange: NSRange = .init(location: 0, length: 0) - + @Published var isPosting: Bool = false @Published var selectedMedias: [PhotosPickerItem] = [] { didSet { if selectedMedias.count > 4 { - selectedMedias = selectedMedias.prefix(4).map{ $0 } + selectedMedias = selectedMedias.prefix(4).map { $0 } } inflateSelectedMedias() } } + @Published var mediasImages: [ImageContainer] = [] @Published var replyToStatus: Status? @Published var embededStatus: Status? @@ -58,31 +60,31 @@ public class StatusEditorViewModel: ObservableObject { var shouldDisablePollButton: Bool { showPoll || !selectedMedias.isEmpty } - + @Published var visibility: Models.Visibility = .pub - + @Published var mentionsSuggestions: [Account] = [] @Published var tagsSuggestions: [Tag] = [] @Published var selectedLanguage: String? private var currentSuggestionRange: NSRange? - + private var embededStatusURL: URL? { return embededStatus?.reblog?.url ?? embededStatus?.url } - + private var uploadTask: Task? - + init(mode: Mode) { self.mode = mode } - + func insertStatusText(text: String) { let string = statusText string.mutableString.insert(text, at: selectedRange.location) statusText = string selectedRange = NSRange(location: selectedRange.location + text.utf16.count, length: 0) } - + func replaceTextWith(text: String, inRange: NSRange) { let string = statusText string.mutableString.deleteCharacters(in: inRange) @@ -90,20 +92,20 @@ public class StatusEditorViewModel: ObservableObject { statusText = string selectedRange = NSRange(location: inRange.location + text.utf16.count, length: 0) } - + func replaceTextWith(text: String) { statusText = .init(string: text) selectedRange = .init(location: text.utf16.count, length: 0) } - + func setInitialLanguageSelection(preference: String?) { switch mode { - case .replyTo(let status), .edit(let status): + case let .replyTo(status), let .edit(status): selectedLanguage = status.language default: break } - + selectedLanguage = selectedLanguage ?? preference ?? currentAccount?.source?.language } @@ -111,7 +113,7 @@ public class StatusEditorViewModel: ObservableObject { let options = pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } return options.isEmpty ? nil : options } - + func postStatus() async -> Status? { guard let client else { return nil } do { @@ -127,7 +129,7 @@ public class StatusEditorViewModel: ObservableObject { visibility: visibility, inReplyToId: mode.replyToStatus?.id, spoilerText: spoilerOn ? spoilerText : nil, - mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id }, + mediaIds: mediasImages.compactMap { $0.mediaAttachement?.id }, poll: pollData, language: selectedLanguage) switch mode { @@ -145,14 +147,14 @@ public class StatusEditorViewModel: ObservableObject { return nil } } - + func prepareStatusText() { switch mode { case let .new(visibility): self.visibility = visibility case let .shareExtension(items): - self.visibility = .pub - self.processItemsProvider(items: items) + visibility = .pub + processItemsProvider(items: items) case let .replyTo(status): var mentionString = "" if (status.reblog?.account.acct ?? status.account.acct) != currentAccount?.acct { @@ -183,19 +185,19 @@ public class StatusEditorViewModel: ObservableObject { spoilerOn = !status.spoilerText.isEmpty spoilerText = status.spoilerText visibility = status.visibility - mediasImages = status.mediaAttachments.map{ .init(image: nil, mediaAttachement: $0, error: nil )} + mediasImages = status.mediaAttachments.map { .init(image: nil, mediaAttachement: $0, error: nil) } case let .quote(status): - self.embededStatus = status + embededStatus = status if let url = embededStatusURL { statusText = .init(string: "\n\nFrom: @\(status.reblog?.account.acct ?? status.account.acct)\n\(url)") selectedRange = .init(location: 0, length: 0) } } } - + private func processText() { statusText.addAttributes([.foregroundColor: UIColor(Color.label)], - range: NSMakeRange(0, statusText.string.utf16.count)) + range: NSMakeRange(0, statusText.string.utf16.count)) let hashtagPattern = "(#+[a-zA-Z0-9(_)]{1,})" let mentionPattern = "(@+[a-zA-Z0-9(_).-]{1,})" let urlPattern = "(?i)https?://(?:www\\.)?\\S+(?:/|\\b)" @@ -204,44 +206,45 @@ public class StatusEditorViewModel: ObservableObject { let hashtagRegex = try NSRegularExpression(pattern: hashtagPattern, options: []) let mentionRegex = try NSRegularExpression(pattern: mentionPattern, options: []) 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 { $0.range } + options: [], + range: range).map { $0.range } ranges.append(contentsOf: mentionRegex.matches(in: statusText.string, options: [], - range: range).map {$0.range}) - + range: range).map { $0.range }) + let urlRanges = urlRegex.matches(in: statusText.string, options: [], - range:range).map { $0.range } + range: range).map { $0.range } - var foundSuggestionRange: Bool = false + var foundSuggestionRange = false for nsRange in ranges { statusText.addAttributes([.foregroundColor: UIColor(theme?.tintColor ?? .brand)], - range: nsRange) + 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 loadAutoCompleteResults(query: String(statusText.string[range])) } } - - if !foundSuggestionRange || ranges.isEmpty{ + + if !foundSuggestionRange || ranges.isEmpty { resetAutoCompletion() } - + for range in urlRanges { statusText.addAttributes([.foregroundColor: UIColor(theme?.tintColor ?? .brand), - .underlineStyle: NSUnderlineStyle.single, + .underlineStyle: NSUnderlineStyle.single, .underlineColor: UIColor(theme?.tintColor ?? .brand)], - range: NSRange(location: range.location, length: range.length)) + range: NSRange(location: range.location, length: range.length)) } - + var attachementsToRemove: [NSRange] = [] - statusText.enumerateAttribute(.attachment, in: range) { attachement, raneg, _ in + statusText.enumerateAttribute(.attachment, in: range) { attachement, _, _ in if let attachement = attachement as? NSTextAttachment, let image = attachement.image { attachementsToRemove.append(range) mediasImages.append(.init(image: image, mediaAttachement: nil, error: nil)) @@ -253,17 +256,16 @@ public class StatusEditorViewModel: ObservableObject { statusText.removeAttribute(.attachment, range: range) } } - } catch { - - } + } catch {} } - + private func processItemsProvider(items: [NSItemProvider]) { Task { var initalText: String = "" for item in items { if let identifiter = item.registeredTypeIdentifiers.first, - let handledItemType = StatusEditorUTTypeSupported(rawValue: identifiter) { + let handledItemType = StatusEditorUTTypeSupported(rawValue: identifiter) + { do { let content = try await handledItemType.loadItemContent(item: item) if let text = content as? String { @@ -271,7 +273,7 @@ public class StatusEditorViewModel: ObservableObject { } else if let image = content as? UIImage { mediasImages.append(.init(image: image, mediaAttachement: nil, error: nil)) } - } catch { } + } catch {} } } if !initalText.isEmpty { @@ -289,17 +291,18 @@ public class StatusEditorViewModel: ObservableObject { pollDuration = .oneDay pollVotingFrequency = .oneVote } - + private func checkEmbed() { if let url = embededStatusURL, - !statusText.string.contains(url.absoluteString) { - self.embededStatus = nil - self.mode = .new(vivibilty: visibility) + !statusText.string.contains(url.absoluteString) + { + embededStatus = nil + mode = .new(vivibilty: visibility) } } - + // MARK: - Autocomplete - + private func loadAutoCompleteResults(query: String) { guard let client, query.utf8.count > 1 else { return } Task { @@ -324,35 +327,33 @@ public class StatusEditorViewModel: ObservableObject { withAnimation { mentionsSuggestions = results?.accounts ?? [] } - break default: break } - } catch { - - } + } catch {} } } - + private func resetAutoCompletion() { tagsSuggestions = [] mentionsSuggestions = [] currentSuggestionRange = nil } - + func selectMentionSuggestion(account: Account) { if let range = currentSuggestionRange { replaceTextWith(text: "@\(account.acct) ", inRange: range) } } - + func selectHashtagSuggestion(tag: Tag) { if let range = currentSuggestionRange { replaceTextWith(text: "#\(tag.name) ", inRange: range) } } - + // MARK: - OpenAI Prompt + func runOpenAI(prompt: OpenAIClient.Prompts) async { do { let client = OpenAIClient() @@ -363,24 +364,25 @@ public class StatusEditorViewModel: ObservableObject { backupStatustext = statusText replaceTextWith(text: text) } - } catch { } + } catch {} } - + // MARK: - Media related function - + private func indexOf(container: ImageContainer) -> Int? { mediasImages.firstIndex(where: { $0.id == container.id }) } - + func inflateSelectedMedias() { - self.mediasImages = [] - + mediasImages = [] + Task { var medias: [ImageContainer] = [] for media in selectedMedias { do { if let data = try await media.loadTransferable(type: Data.self), - let image = UIImage(data: data) { + let image = UIImage(data: data) + { medias.append(.init(image: image, mediaAttachement: nil, error: nil)) } } catch { @@ -393,7 +395,7 @@ public class StatusEditorViewModel: ObservableObject { } } } - + private func processMediasToUpload() { uploadTask?.cancel() let mediasCopy = mediasImages @@ -405,7 +407,7 @@ public class StatusEditorViewModel: ObservableObject { } } } - + func upload(container: ImageContainer) async { if let index = indexOf(container: container) { let originalContainer = mediasImages[index] @@ -427,7 +429,7 @@ public class StatusEditorViewModel: ObservableObject { } } } - + func addDescription(container: ImageContainer, description: String) async { guard let client, let attachment = container.mediaAttachement else { return } if let index = indexOf(container: container) { @@ -435,12 +437,10 @@ public class StatusEditorViewModel: ObservableObject { let media: MediaAttachement = try await client.put(endpoint: Media.media(id: attachment.id, description: description)) mediasImages[index] = .init(image: nil, mediaAttachement: media, error: nil) - } catch { - - } + } catch {} } } - + private func uploadMedia(data: Data) async throws -> MediaAttachement? { guard let client else { return nil } return try await client.mediaUpload(endpoint: Media.medias, diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift index ce74fbde..859e65f5 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift @@ -1,15 +1,15 @@ import Models import UIKit -extension StatusEditorViewModel { - public enum Mode { +public extension StatusEditorViewModel { + enum Mode { case replyTo(status: Status) case new(vivibilty: Visibility) case edit(status: Status) case quote(status: Status) case mention(account: Account, visibility: Visibility) case shareExtension(items: [NSItemProvider]) - + var isInShareExtension: Bool { switch self { case .shareExtension: @@ -18,7 +18,7 @@ extension StatusEditorViewModel { return false } } - + var isEditing: Bool { switch self { case .edit: @@ -27,16 +27,16 @@ extension StatusEditorViewModel { return false } } - + var replyToStatus: Status? { switch self { - case let .replyTo(status): - return status - default: - return nil + case let .replyTo(status): + return status + default: + return nil } } - + var title: String { switch self { case .new, .mention, .shareExtension: diff --git a/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift b/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift index 7b6b583c..320ec2af 100644 --- a/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift +++ b/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift @@ -1,18 +1,18 @@ -import SwiftUI -import Models import DesignSystem import EmojiText +import Models +import SwiftUI @MainActor public struct StatusEmbededView: View { @EnvironmentObject private var theme: Theme - + public let status: Status - + public init(status: Status) { self.status = status } - + public var body: some View { HStack { VStack(alignment: .leading) { @@ -30,7 +30,7 @@ public struct StatusEmbededView: View { ) .padding(.top, 8) } - + private func makeAccountView(account: Account) -> some View { HStack(alignment: .center) { AvatarView(url: account.avatar, size: .embed) @@ -40,8 +40,8 @@ public struct StatusEmbededView: View { .fontWeight(.semibold) Group { Text("@\(account.acct)") + - Text(" ⸱ ") + - Text(status.reblog?.createdAt.formatted ?? status.createdAt.formatted) + Text(" ⸱ ") + + Text(status.reblog?.createdAt.formatted ?? status.createdAt.formatted) } .font(.caption) .foregroundColor(.gray) diff --git a/Packages/Status/Sources/Status/Ext/Visibility.swift b/Packages/Status/Sources/Status/Ext/Visibility.swift index 774f6d80..970f4510 100644 --- a/Packages/Status/Sources/Status/Ext/Visibility.swift +++ b/Packages/Status/Sources/Status/Ext/Visibility.swift @@ -1,11 +1,11 @@ import Models -extension Visibility { - public static var supportDefault: [Visibility] { +public extension Visibility { + static var supportDefault: [Visibility] { [.pub, .priv, .unlisted] } - - public var iconName: String { + + var iconName: String { switch self { case .pub: return "globe.americas" @@ -17,8 +17,8 @@ extension Visibility { return "tray.full" } } - - public var title: String { + + var title: String { switch self { case .pub: return "Everyone" diff --git a/Packages/Status/Sources/Status/List/StatusesFetcher.swift b/Packages/Status/Sources/Status/List/StatusesFetcher.swift index 90027e94..9a124e50 100644 --- a/Packages/Status/Sources/Status/List/StatusesFetcher.swift +++ b/Packages/Status/Sources/Status/List/StatusesFetcher.swift @@ -1,10 +1,11 @@ -import SwiftUI import Models +import SwiftUI public enum StatusesState { public enum PagingState { case hasNextPage, loadingNextPage, none } + case loading case display(statuses: [Status], nextPageState: StatusesState.PagingState) case error(error: Error) diff --git a/Packages/Status/Sources/Status/List/StatusesListView.swift b/Packages/Status/Sources/Status/List/StatusesListView.swift index 4349d8a4..c6c061cc 100644 --- a/Packages/Status/Sources/Status/List/StatusesListView.swift +++ b/Packages/Status/Sources/Status/List/StatusesListView.swift @@ -1,17 +1,17 @@ -import SwiftUI +import DesignSystem import Models import Shimmer -import DesignSystem +import SwiftUI public struct StatusesListView: View where Fetcher: StatusesFetcher { @ObservedObject private var fetcher: Fetcher private let isRemote: Bool - + public init(fetcher: Fetcher, isRemote: Bool = false) { self.fetcher = fetcher self.isRemote = isRemote } - + public var body: some View { Group { switch fetcher.statusesState { @@ -32,7 +32,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { await fetcher.fetchStatuses() } } - + case let .display(statuses, nextPageState): ForEach(statuses, id: \.viewId) { status in StatusRowView(viewModel: .init(status: status, isCompact: false, isRemote: isRemote)) @@ -41,7 +41,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { Divider() .padding(.vertical, .dividerPadding) } - + switch nextPageState { case .hasNextPage: loadingRow @@ -59,7 +59,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { } .frame(maxWidth: .maxColumnWidth) } - + private var loadingRow: some View { HStack { Spacer() diff --git a/Packages/Status/Sources/Status/Media/VideoPlayerView.swift b/Packages/Status/Sources/Status/Media/VideoPlayerView.swift index f24b7047..e2608864 100644 --- a/Packages/Status/Sources/Status/Media/VideoPlayerView.swift +++ b/Packages/Status/Sources/Status/Media/VideoPlayerView.swift @@ -1,14 +1,14 @@ -import SwiftUI import AVKit +import SwiftUI class VideoPlayerViewModel: ObservableObject { @Published var player: AVPlayer? private let url: URL - + init(url: URL) { self.url = url } - + func preparePlayer() { player = .init(url: url) player?.isMuted = true @@ -16,11 +16,11 @@ class VideoPlayerViewModel: ObservableObject { guard let player else { return } NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { [weak self] _ in - self?.player?.seek(to: CMTime.zero) - self?.player?.play() + self?.player?.seek(to: CMTime.zero) + self?.player?.play() } } - + deinit { NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: self.player) } diff --git a/Packages/Status/Sources/Status/Poll/StatusPollView.swift b/Packages/Status/Sources/Status/Poll/StatusPollView.swift index a4946f21..068dd1cd 100644 --- a/Packages/Status/Sources/Status/Poll/StatusPollView.swift +++ b/Packages/Status/Sources/Status/Poll/StatusPollView.swift @@ -1,29 +1,29 @@ +import DesignSystem +import Env import Models import Network import SwiftUI -import Env -import DesignSystem public struct StatusPollView: View { enum Constants { static let barHeight: CGFloat = 30 } - + @EnvironmentObject private var theme: Theme @EnvironmentObject private var client: Client @EnvironmentObject private var currentInstance: CurrentInstance @StateObject private var viewModel: StatusPollViewModel - + public init(poll: Poll) { _viewModel = StateObject(wrappedValue: .init(poll: poll)) } - + private func widthForOption(option: Poll.Option, proxy: GeometryProxy) -> CGFloat { let totalWidth = proxy.frame(in: .local).width let ratio = CGFloat(option.votesCount) / CGFloat(viewModel.poll.votesCount) return totalWidth * ratio } - + private func percentForOption(option: Poll.Option) -> Int { let ratio = (Float(option.votesCount) / Float(viewModel.poll.votesCount)) * 100 if ratio.isNaN { @@ -31,14 +31,14 @@ public struct StatusPollView: View { } return Int(round(ratio)) } - + private func isSelected(option: Poll.Option) -> Bool { for vote in viewModel.votes { return viewModel.poll.options.firstIndex(where: { $0.id == option.id }) == vote } return false } - + public var body: some View { VStack(alignment: .leading) { ForEach(viewModel.poll.options) { option in @@ -62,7 +62,7 @@ public struct StatusPollView: View { } } } - + private var footerView: some View { HStack(spacing: 0) { Text("\(viewModel.poll.votesCount) votes") @@ -77,14 +77,15 @@ public struct StatusPollView: View { .font(.footnote) .foregroundColor(.gray) } - + @ViewBuilder private func makeBarView(for option: Poll.Option) -> some View { let isSelected = isSelected(option: option) Button { if !viewModel.poll.expired, viewModel.votes.isEmpty, - let index = viewModel.poll.options.firstIndex(where: { $0.id == option.id }) { + let index = viewModel.poll.options.firstIndex(where: { $0.id == option.id }) + { withAnimation { viewModel.votes.append(index) Task { @@ -111,7 +112,7 @@ public struct StatusPollView: View { .foregroundColor(theme.tintColor.opacity(0.40)) .frame(height: Constants.barHeight) .clipShape(RoundedRectangle(cornerRadius: 8)) - + HStack { if isSelected { Image(systemName: "checkmark.circle.fill") diff --git a/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift b/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift index 50ae8605..8803d67d 100644 --- a/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift +++ b/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift @@ -1,32 +1,32 @@ -import SwiftUI -import Network import Models +import Network +import SwiftUI @MainActor public class StatusPollViewModel: ObservableObject { public var client: Client? public var instance: Instance? - + @Published var poll: Poll @Published var votes: [Int] = [] - + var showResults: Bool { !votes.isEmpty || poll.expired } - + public init(poll: Poll) { self.poll = poll - self.votes = poll.ownVotes ?? [] + votes = poll.ownVotes ?? [] } - + public func fetchPoll() async { guard let client else { return } do { poll = try await client.get(endpoint: Polls.poll(id: poll.id)) votes = poll.ownVotes ?? [] - } catch { } + } catch {} } - + public func postVotes() async { guard let client, !poll.expired else { return } do { diff --git a/Packages/Status/Sources/Status/Row/StatusActionsView.swift b/Packages/Status/Sources/Status/Row/StatusActionsView.swift index 2ea02a2e..c77dc1e2 100644 --- a/Packages/Status/Sources/Status/Row/StatusActionsView.swift +++ b/Packages/Status/Sources/Status/Row/StatusActionsView.swift @@ -1,21 +1,21 @@ -import SwiftUI -import Models -import Env -import Network import DesignSystem +import Env +import Models +import Network +import SwiftUI struct StatusActionsView: View { @Environment(\.openURL) private var openURL @EnvironmentObject private var theme: Theme @EnvironmentObject private var routeurPath: RouterPath @ObservedObject var viewModel: StatusRowViewModel - + let generator = UINotificationFeedbackGenerator() - + @MainActor enum Actions: CaseIterable { case respond, boost, favourite, bookmark, share - + func iconName(viewModel: StatusRowViewModel) -> String { switch self { case .respond: @@ -30,7 +30,7 @@ struct StatusActionsView: View { return "square.and.arrow.up" } } - + func count(viewModel: StatusRowViewModel, theme: Theme) -> Int? { if theme.statusActionsDisplay == .discret { return nil @@ -46,7 +46,7 @@ struct StatusActionsView: View { return nil } } - + func tintColor(viewModel: StatusRowViewModel, theme: Theme) -> Color? { switch self { case .respond, .share: @@ -60,7 +60,7 @@ struct StatusActionsView: View { } } } - + var body: some View { VStack(spacing: 12) { HStack { @@ -95,15 +95,15 @@ struct StatusActionsView: View { } } } - + @ViewBuilder private var summaryView: some View { Divider() HStack { Text(viewModel.status.createdAt.asDate, style: .date) + - Text(" at ") + - Text(viewModel.status.createdAt.asDate, style: .time) + - Text(" ·") + Text(" at ") + + Text(viewModel.status.createdAt.asDate, style: .time) + + Text(" ·") Image(systemName: viewModel.status.visibility.iconName) Spacer() Text(viewModel.status.application?.name ?? "") @@ -116,7 +116,7 @@ struct StatusActionsView: View { } .font(.caption) .foregroundColor(.gray) - + if viewModel.favouritesCount > 0 { Divider() NavigationLink(value: RouteurDestinations.favouritedBy(id: viewModel.status.id)) { @@ -136,7 +136,7 @@ struct StatusActionsView: View { } } } - + private func handleAction(action: Actions) { Task { generator.notificationOccurred(.success) diff --git a/Packages/Status/Sources/Status/Row/StatusCardView.swift b/Packages/Status/Sources/Status/Row/StatusCardView.swift index 9d7b53df..2825951d 100644 --- a/Packages/Status/Sources/Status/Row/StatusCardView.swift +++ b/Packages/Status/Sources/Status/Row/StatusCardView.swift @@ -1,18 +1,18 @@ -import SwiftUI -import Models -import Shimmer -import NukeUI import DesignSystem +import Models +import NukeUI +import Shimmer +import SwiftUI public struct StatusCardView: View { @EnvironmentObject private var theme: Theme @Environment(\.openURL) private var openURL let card: Card - + public init(card: Card) { self.card = card } - + public var body: some View { if let title = card.title { VStack(alignment: .leading) { diff --git a/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift b/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift index c1caca75..67196c2c 100644 --- a/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift +++ b/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift @@ -1,15 +1,15 @@ -import SwiftUI -import Models -import Env -import Shimmer -import NukeUI import DesignSystem +import Env +import Models +import NukeUI +import Shimmer +import SwiftUI public struct StatusMediaPreviewView: View { @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var quickLook: QuickLook @EnvironmentObject private var theme: Theme - + public let attachements: [MediaAttachement] public let sensitive: Bool public let isNotifications: Bool @@ -19,7 +19,7 @@ public struct StatusMediaPreviewView: View { @State private var altTextDisplayed: String? @State private var isAltAlertDisplayed: Bool = false @State private var isHidingMedia: Bool = false - + private var imageMaxHeight: CGFloat { if isNotifications { if UIDevice.current.userInterfaceIdiom == .pad { @@ -35,7 +35,7 @@ public struct StatusMediaPreviewView: View { } return attachements.count > 2 ? 100 : 200 } - + private func size(for media: MediaAttachement) -> CGSize? { if isNotifications { return .init(width: 50, height: 50) @@ -44,12 +44,13 @@ public struct StatusMediaPreviewView: View { return .init(width: 100, height: 100) } if let width = media.meta?.original?.width, - let height = media.meta?.original?.height { + let height = media.meta?.original?.height + { return .init(width: CGFloat(width), height: CGFloat(height)) } return nil } - + private func imageSize(from: CGSize, newWidth: CGFloat) -> CGSize { if isNotifications { return .init(width: 50, height: 50) @@ -58,14 +59,14 @@ public struct StatusMediaPreviewView: View { let newHeight = from.height * ratio return .init(width: newWidth, height: newHeight) } - + public var body: some View { Group { if attachements.count == 1, let attachement = attachements.first { makeFeaturedImagePreview(attachement: attachement) .onTapGesture { Task { - await quickLook.prepareFor(urls: attachements.compactMap{ $0.url }, selectedURL: attachement.url!) + await quickLook.prepareFor(urls: attachements.compactMap { $0.url }, selectedURL: attachement.url!) } } } else { @@ -95,7 +96,7 @@ public struct StatusMediaPreviewView: View { quickLookLoadingView .transition(.opacity) } - + if isHidingMedia { sensitiveMediaOverlay .transition(.opacity) @@ -103,7 +104,7 @@ public struct StatusMediaPreviewView: View { } .alert("Image description", isPresented: $isAltAlertDisplayed) { - Button("Ok", action: { }) + Button("Ok", action: {}) } message: { Text(altTextDisplayed ?? "") } @@ -116,16 +117,15 @@ public struct StatusMediaPreviewView: View { isHidingMedia = false } } - } - + @ViewBuilder private func makeAttachementView(for index: Int) -> some View { if attachements.count > index { makePreview(attachement: attachements[index]) } } - + @ViewBuilder private func makeFeaturedImagePreview(attachement: MediaAttachement) -> some View { switch attachement.supportedType { @@ -133,7 +133,8 @@ public struct StatusMediaPreviewView: View { if theme.statusDisplayStyle == .large, let size = size(for: attachement), UIDevice.current.userInterfaceIdiom != .pad, - UIDevice.current.userInterfaceIdiom != .mac { + UIDevice.current.userInterfaceIdiom != .mac + { let avatarColumnWidth = theme.avatarPosition == .leading ? AvatarView.Size.status.size.width + .statusColumnsSpacing : 0 let availableWidth = UIScreen.main.bounds.width - (.layoutPadding * 2) - avatarColumnWidth let newSize = imageSize(from: size, @@ -169,20 +170,21 @@ public struct StatusMediaPreviewView: View { } } else { AsyncImage( - url: attachement.url, - content: { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(maxHeight: imageMaxHeight) - .cornerRadius(4) - }, - placeholder: { - RoundedRectangle(cornerRadius: 4) - .fill(Color.gray) - .frame(maxHeight: imageMaxHeight) - .shimmering() - }) + url: attachement.url, + content: { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxHeight: imageMaxHeight) + .cornerRadius(4) + }, + placeholder: { + RoundedRectangle(cornerRadius: 4) + .fill(Color.gray) + .frame(maxHeight: imageMaxHeight) + .shimmering() + } + ) } case .gifv, .video, .audio: if let url = attachement.url { @@ -193,7 +195,7 @@ public struct StatusMediaPreviewView: View { EmptyView() } } - + @ViewBuilder private func makePreview(attachement: MediaAttachement) -> some View { if let type = attachement.supportedType { @@ -236,7 +238,7 @@ public struct StatusMediaPreviewView: View { case .gifv, .video, .audio: if let url = attachement.url { VideoPlayerView(viewModel: .init(url: url)) - .frame(width: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width) + .frame(width: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width) .frame(height: imageMaxHeight) } } @@ -246,12 +248,12 @@ public struct StatusMediaPreviewView: View { } .onTapGesture { Task { - await quickLook.prepareFor(urls: attachements.compactMap{ $0.url }, selectedURL: attachement.url!) + await quickLook.prepareFor(urls: attachements.compactMap { $0.url }, selectedURL: attachement.url!) } } } } - + private var quickLookLoadingView: some View { ZStack(alignment: .center) { VStack { @@ -266,7 +268,7 @@ public struct StatusMediaPreviewView: View { } .background(.ultraThinMaterial) } - + private var sensitiveMediaOverlay: some View { Rectangle() .background(.ultraThinMaterial) @@ -287,14 +289,14 @@ public struct StatusMediaPreviewView: View { } } } - + private var cornerSensitiveButton: some View { Button { withAnimation { isHidingMedia = true } } label: { - Image(systemName:"eye.slash") + Image(systemName: "eye.slash") } .position(x: 30, y: 30) .buttonStyle(.borderedProminent) diff --git a/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift b/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift index 375c3c9d..b8eb0a3e 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift @@ -1,15 +1,15 @@ +import Env import Foundation import SwiftUI -import Env struct StatusRowContextMenu: View { @EnvironmentObject private var account: CurrentAccount @EnvironmentObject private var routeurPath: RouterPath - + @Environment(\.openURL) var openURL - + @ObservedObject var viewModel: StatusRowViewModel - + var body: some View { if !viewModel.isRemote { Button { Task { @@ -46,7 +46,7 @@ struct StatusRowContextMenu: View { Label("Reply", systemImage: "arrowshape.turn.up.left") } } - + if viewModel.status.visibility == .pub, !viewModel.isRemote { Button { routeurPath.presentedSheet = .quoteStatusEditor(status: viewModel.status) @@ -54,21 +54,21 @@ struct StatusRowContextMenu: View { Label("Quote this post", systemImage: "quote.bubble") } } - + Divider() - + if let url = viewModel.status.reblog?.url ?? viewModel.status.url { - ShareLink(item: url) { - Label("Share this post", systemImage: "square.and.arrow.up") - } + ShareLink(item: url) { + Label("Share this post", systemImage: "square.and.arrow.up") + } } - + if let url = viewModel.status.reblog?.url ?? viewModel.status.url { Button { openURL(url) } label: { Label("View in Browser", systemImage: "safari") } } - + Button { UIPasteboard.general.string = viewModel.status.content.asRawText } label: { @@ -86,7 +86,7 @@ struct StatusRowContextMenu: View { } } } label: { - Label(viewModel.isPinned ? "Unpin": "Pin", systemImage: viewModel.isPinned ? "pin.fill" : "pin") + Label(viewModel.isPinned ? "Unpin" : "Pin", systemImage: viewModel.isPinned ? "pin.fill" : "pin") } Button { routeurPath.presentedSheet = .editStatusEditor(status: viewModel.status) diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 82ae976b..b13e1066 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -1,10 +1,10 @@ -import SwiftUI -import Models -import Env import DesignSystem +import EmojiText +import Env +import Models import Network import Shimmer -import EmojiText +import SwiftUI public struct StatusRowView: View { @Environment(\.redactionReasons) private var reasons @@ -14,11 +14,11 @@ public struct StatusRowView: View { @EnvironmentObject private var client: Client @EnvironmentObject private var routeurPath: RouterPath @StateObject var viewModel: StatusRowViewModel - + public init(viewModel: StatusRowViewModel) { _viewModel = StateObject(wrappedValue: viewModel) } - + public var body: some View { if viewModel.isFiltered, let filter = viewModel.filter { switch filter.filter.filterAction { @@ -31,7 +31,8 @@ public struct StatusRowView: View { HStack(alignment: .top, spacing: .statusColumnsSpacing) { if !viewModel.isCompact, theme.avatarPosition == .leading, - let status: AnyStatus = viewModel.status.reblog ?? viewModel.status { + let status: AnyStatus = viewModel.status.reblog ?? viewModel.status + { Button { routeurPath.navigate(to: .accountDetailWithAccount(account: status.account)) } label: { @@ -78,7 +79,7 @@ public struct StatusRowView: View { } } } - + private func makeFilterView(filter: Filter) -> some View { HStack { Text("Filtered by: \(filter.title)") @@ -91,12 +92,12 @@ public struct StatusRowView: View { } } } - + @ViewBuilder private var reblogView: some View { if viewModel.status.reblog != nil { HStack(spacing: 2) { - Image(systemName:"arrow.left.arrow.right.circle.fill") + Image(systemName: "arrow.left.arrow.right.circle.fill") AvatarView(url: viewModel.status.account.avatar, size: .boost) if viewModel.status.account.username != account.account?.username { EmojiTextApp(viewModel.status.account.safeDisplayName.asMarkdown, emojis: viewModel.status.account.emojis) @@ -119,13 +120,14 @@ public struct StatusRowView: View { } } } - + @ViewBuilder var replyView: some View { if let accountId = viewModel.status.inReplyToAccountId, - let mention = viewModel.status.mentions.first(where: { $0.id == accountId}) { + let mention = viewModel.status.mentions.first(where: { $0.id == accountId }) + { HStack(spacing: 2) { - Image(systemName:"arrowshape.turn.up.left.fill") + Image(systemName: "arrowshape.turn.up.left.fill") Text("Replied to") Text(mention.username) } @@ -143,7 +145,7 @@ public struct StatusRowView: View { } } } - + private var statusView: some View { VStack(alignment: .leading, spacing: 8) { if let status: AnyStatus = viewModel.status.reblog ?? viewModel.status { @@ -172,7 +174,7 @@ public struct StatusRowView: View { } } } - + private func makeStatusContentView(status: AnyStatus) -> some View { Group { if !status.spoilerText.isEmpty { @@ -183,7 +185,7 @@ public struct StatusRowView: View { viewModel.displaySpoiler.toggle() } } label: { - Text(viewModel.displaySpoiler ? "Show more" : "Show less") + Text(viewModel.displaySpoiler ? "Show more" : "Show less") } .buttonStyle(.bordered) } @@ -196,7 +198,7 @@ public struct StatusRowView: View { }) Spacer() } - + if !reasons.contains(.placeholder) { if !viewModel.isCompact, !viewModel.isEmbedLoading, let embed = viewModel.embededStatus { StatusEmbededView(status: embed) @@ -206,11 +208,11 @@ public struct StatusRowView: View { .shimmering() } } - + if let poll = status.poll { StatusPollView(poll: poll) } - + if !status.mediaAttachments.isEmpty { if theme.statusDisplayStyle == .compact { HStack { @@ -224,20 +226,21 @@ public struct StatusRowView: View { StatusMediaPreviewView(attachements: status.mediaAttachments, sensitive: status.sensitive, isNotifications: viewModel.isCompact) - .padding(.vertical, 4) + .padding(.vertical, 4) } } if let card = status.card, viewModel.embededStatus?.url != status.card?.url, status.mediaAttachments.isEmpty, !viewModel.isEmbedLoading, - theme.statusDisplayStyle == .large { + theme.statusDisplayStyle == .large + { StatusCardView(card: card) } } } } - + @ViewBuilder private func accountView(status: AnyStatus) -> some View { HStack(alignment: .center) { @@ -250,17 +253,17 @@ public struct StatusRowView: View { .fontWeight(.semibold) Group { Text("@\(status.account.acct)") + - Text(" ⸱ ") + - Text(status.createdAt.formatted) + - Text(" ⸱ ") + - Text(Image(systemName: viewModel.status.visibility.iconName)) + Text(" ⸱ ") + + Text(status.createdAt.formatted) + + Text(" ⸱ ") + + Text(Image(systemName: viewModel.status.visibility.iconName)) } .font(.footnote) .foregroundColor(.gray) } } } - + private var menuButton: some View { Menu { StatusRowContextMenu(viewModel: viewModel) diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index 6f96f019..ee8b1365 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -1,7 +1,7 @@ -import SwiftUI +import Env import Models import Network -import Env +import SwiftUI @MainActor public class StatusRowViewModel: ObservableObject { @@ -10,7 +10,7 @@ public class StatusRowViewModel: ObservableObject { let isFocused: Bool let isRemote: Bool let showActions: Bool - + @Published var favouritesCount: Int @Published var isFavourited: Bool @Published var isReblogged: Bool @@ -22,42 +22,43 @@ public class StatusRowViewModel: ObservableObject { @Published var displaySpoiler: Bool = false @Published var isEmbedLoading: Bool = true @Published var isFiltered: Bool = false - + var filter: Filtered? { status.reblog?.filtered?.first ?? status.filtered?.first } - + var client: Client? - + public init(status: Status, isCompact: Bool = false, isFocused: Bool = false, isRemote: Bool = false, - showActions: Bool = true) { + showActions: Bool = true) + { self.status = status self.isCompact = isCompact self.isFocused = isFocused self.isRemote = isRemote self.showActions = showActions if let reblog = status.reblog { - self.isFavourited = reblog.favourited == true - self.isReblogged = reblog.reblogged == true - self.isPinned = reblog.pinned == true - self.isBookmarked = reblog.bookmarked == true + isFavourited = reblog.favourited == true + isReblogged = reblog.reblogged == true + isPinned = reblog.pinned == true + isBookmarked = reblog.bookmarked == true } else { - self.isFavourited = status.favourited == true - self.isReblogged = status.reblogged == true - self.isPinned = status.pinned == true - self.isBookmarked = status.bookmarked == true + isFavourited = status.favourited == true + isReblogged = status.reblogged == true + isPinned = status.pinned == true + isBookmarked = status.bookmarked == true } - self.favouritesCount = status.reblog?.favouritesCount ?? status.favouritesCount - self.reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount - self.repliesCount = status.reblog?.repliesCount ?? status.repliesCount - self.displaySpoiler = !(status.reblog?.spoilerText ?? status.spoilerText).isEmpty - - self.isFiltered = filter != nil + favouritesCount = status.reblog?.favouritesCount ?? status.favouritesCount + reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount + repliesCount = status.reblog?.repliesCount ?? status.repliesCount + displaySpoiler = !(status.reblog?.spoilerText ?? status.spoilerText).isEmpty + + isFiltered = filter != nil } - + func navigateToDetail(routeurPath: RouterPath) { if isRemote, let url = status.reblog?.url ?? status.url { routeurPath.navigate(to: .remoteStatusDetail(url: url)) @@ -65,13 +66,14 @@ public class StatusRowViewModel: ObservableObject { routeurPath.navigate(to: .statusDetail(id: status.reblog?.id ?? status.id)) } } - + func loadEmbededStatus() async { guard let client, let urls = status.content.findStatusesURLs(), !urls.isEmpty, let url = urls.first, - client.hasConnection(with: url) else { + client.hasConnection(with: url) + else { isEmbedLoading = false return } @@ -87,7 +89,7 @@ public class StatusRowViewModel: ObservableObject { type: "statuses", offset: 0, following: nil), - forceVersion: .v2) + forceVersion: .v2) embed = results.statuses.first } withAnimation { @@ -98,7 +100,7 @@ public class StatusRowViewModel: ObservableObject { isEmbedLoading = false } } - + func favourite() async { guard let client, client.isAuth else { return } isFavourited = true @@ -111,7 +113,7 @@ public class StatusRowViewModel: ObservableObject { favouritesCount -= 1 } } - + func unFavourite() async { guard let client, client.isAuth else { return } isFavourited = false @@ -124,7 +126,7 @@ public class StatusRowViewModel: ObservableObject { favouritesCount += 1 } } - + func reblog() async { guard let client, client.isAuth else { return } isReblogged = true @@ -137,7 +139,7 @@ public class StatusRowViewModel: ObservableObject { reblogsCount -= 1 } } - + func unReblog() async { guard let client, client.isAuth else { return } isReblogged = false @@ -150,7 +152,7 @@ public class StatusRowViewModel: ObservableObject { reblogsCount += 1 } } - + func pin() async { guard let client, client.isAuth else { return } isPinned = true @@ -161,7 +163,7 @@ public class StatusRowViewModel: ObservableObject { isPinned = false } } - + func unPin() async { guard let client, client.isAuth else { return } isPinned = false @@ -172,7 +174,7 @@ public class StatusRowViewModel: ObservableObject { isPinned = true } } - + func bookmark() async { guard let client, client.isAuth else { return } isBookmarked = true @@ -183,7 +185,7 @@ public class StatusRowViewModel: ObservableObject { isBookmarked = false } } - + func unbookmark() async { guard let client, client.isAuth else { return } isBookmarked = false @@ -194,14 +196,14 @@ public class StatusRowViewModel: ObservableObject { isBookmarked = true } } - + func delete() async { guard let client else { return } do { _ = try await client.delete(endpoint: Statuses.status(id: status.id)) - } catch { } + } catch {} } - + private func updateFromStatus(status: Status) { if let reblog = status.reblog { isFavourited = reblog.favourited == true diff --git a/Packages/Timeline/Package.swift b/Packages/Timeline/Package.swift index 2d617052..4f7b4d21 100644 --- a/Packages/Timeline/Package.swift +++ b/Packages/Timeline/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "Timeline", - targets: ["Timeline"]), + targets: ["Timeline"] + ), ], dependencies: [ .package(name: "Network", path: "../Network"), @@ -28,11 +29,12 @@ let package = Package( .product(name: "Models", package: "Models"), .product(name: "Env", package: "Env"), .product(name: "Status", package: "Status"), - .product(name: "DesignSystem", package: "DesignSystem") - ]), + .product(name: "DesignSystem", package: "DesignSystem"), + ] + ), .testTarget( name: "TimelineTests", - dependencies: ["Timeline"]), + dependencies: ["Timeline"] + ), ] ) - diff --git a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift index 1b868fe7..41426411 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift @@ -7,18 +7,18 @@ public enum TimelineFilter: Hashable, Equatable { case hashtag(tag: String, accountId: String?) case list(list: List) case remoteLocal(server: String) - + public func hash(into hasher: inout Hasher) { hasher.combine(title()) } - + public static func availableTimeline(client: Client) -> [TimelineFilter] { if !client.isAuth { return [.local, .federated, .trending] } return [.home, .local, .federated, .trending] } - + public func title() -> String { switch self { case .federated: @@ -37,7 +37,7 @@ public enum TimelineFilter: Hashable, Equatable { return server } } - + public func iconName() -> String? { switch self { case .federated: @@ -48,7 +48,7 @@ public enum TimelineFilter: Hashable, Equatable { return "chart.line.uptrend.xyaxis" case .home: return "house" - case .list(_): + case .list: return "list.bullet" case .remoteLocal: return "dot.radiowaves.right" @@ -56,7 +56,7 @@ public enum TimelineFilter: Hashable, Equatable { return nil } } - + public func endpoint(sinceId: String?, maxId: String?, minId: String?, offset: Int?) -> Endpoint { switch self { case .federated: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false) diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index ceb6b10a..d7701dc8 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -1,36 +1,36 @@ -import SwiftUI -import Network -import Models -import Shimmer -import Status import DesignSystem import Env +import Models +import Network +import Shimmer +import Status +import SwiftUI public struct TimelineView: View { private enum Constants { static let scrollToTop = "top" } - + @Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var theme: Theme @EnvironmentObject private var account: CurrentAccount @EnvironmentObject private var watcher: StreamWatcher @EnvironmentObject private var client: Client @EnvironmentObject private var routerPath: RouterPath - + @StateObject private var viewModel = TimelineViewModel() @State private var scrollProxy: ScrollViewProxy? @Binding var timeline: TimelineFilter @Binding var scrollToTopSignal: Int - + private let feedbackGenerator = UIImpactFeedbackGenerator() - + public init(timeline: Binding, scrollToTopSignal: Binding) { _timeline = timeline _scrollToTopSignal = scrollToTopSignal } - + public var body: some View { ScrollViewReader { proxy in ZStack(alignment: .top) { @@ -102,7 +102,7 @@ public struct TimelineView: View { } }) } - + @ViewBuilder private func makePendingNewPostsView(proxy: ScrollViewProxy) -> some View { if !viewModel.pendingStatuses.isEmpty { @@ -134,7 +134,7 @@ public struct TimelineView: View { .padding(.top, 6) } } - + @ViewBuilder private var tagHeaderView: some View { if let tag = viewModel.tag { @@ -156,7 +156,7 @@ public struct TimelineView: View { } } } label: { - Text(tag.following ? "Following": "Follow") + Text(tag.following ? "Following" : "Follow") }.buttonStyle(.bordered) } .padding(.horizontal, .layoutPadding) diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index f834b228..6f14c099 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -1,8 +1,8 @@ -import SwiftUI -import Network -import Models -import Status import Env +import Models +import Network +import Status +import SwiftUI @MainActor class TimelineViewModel: ObservableObject, StatusesFetcher { @@ -13,10 +13,10 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { } } } - + // Internal source of truth for a timeline. private var statuses: [Status] = [] - + @Published var statusesState: StatusesState = .loading @Published var timeline: TimelineFilter = .federated { didSet { @@ -36,34 +36,35 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { } } } + @Published var tag: Tag? - + enum PendingStatusesState { case refresh, stream } - + @Published var pendingStatuses: [Status] = [] @Published var pendingStatusesState: PendingStatusesState = .stream - + var pendingStatusesButtonTitle: String { switch pendingStatusesState { case .stream, .refresh: return "\(pendingStatuses.count) new posts" } } - + var pendingStatusesEnabled: Bool { timeline == .home } - + var serverName: String { client?.server ?? "Error" } - + func fetchStatuses() async { await fetchStatuses(userIntent: false) } - + func fetchStatuses(userIntent: Bool) async { guard let client else { return } do { @@ -99,7 +100,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { print("timeline parse error: \(error)") } } - + func fetchNewPages(minId: String, maxPages: Int) async -> [Status] { guard let client else { return [] } var pagesLoaded = 0 @@ -110,8 +111,9 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { maxId: nil, minId: latestMinId, offset: statuses.count)), - !newStatuses.isEmpty, - pagesLoaded < maxPages { + !newStatuses.isEmpty, + pagesLoaded < maxPages + { pagesLoaded += 1 allStatuses.insert(contentsOf: newStatuses, at: 0) latestMinId = newStatuses.first?.id ?? "" @@ -121,7 +123,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { } return allStatuses } - + func fetchNextPage() async { guard let client else { return } do { @@ -137,19 +139,20 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { statusesState = .error(error: error) } } - + func fetchTag(id: String) async { guard let client else { return } do { tag = try await client.get(endpoint: Tags.tag(id: id)) } catch {} } - + func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) { if let event = event as? StreamEventUpdate, pendingStatusesEnabled, !statuses.contains(where: { $0.id == event.status.id }), - !pendingStatuses.contains(where: { $0.id == event.status.id }){ + !pendingStatuses.contains(where: { $0.id == event.status.id }) + { if event.status.account.id == currentAccount.account?.id, pendingStatuses.isEmpty { withAnimation { statuses.insert(event.status, at: 0) @@ -171,7 +174,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { } } } - + func displayPendingStatuses() { guard timeline == .home else { return } pendingStatuses = pendingStatuses.filter { status in @@ -181,7 +184,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { pendingStatuses = [] statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) } - + func dequeuePendingStatuses() { guard timeline == .home else { return } if pendingStatuses.count > 1 { diff --git a/Packages/Timeline/Tests/TimelineTests/TimelineTests.swift b/Packages/Timeline/Tests/TimelineTests/TimelineTests.swift index 2a366067..cf9f504d 100644 --- a/Packages/Timeline/Tests/TimelineTests/TimelineTests.swift +++ b/Packages/Timeline/Tests/TimelineTests/TimelineTests.swift @@ -1,11 +1,11 @@ -import XCTest @testable import Timeline +import XCTest final class TimelineTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(Timeline().text, "Hello, World!") - } + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Timeline().text, "Hello, World!") + } }