diff --git a/IceCubesActionExtension/ActionRequestHandler.swift b/IceCubesActionExtension/ActionRequestHandler.swift index 13c24963..381b6bd2 100644 --- a/IceCubesActionExtension/ActionRequestHandler.swift +++ b/IceCubesActionExtension/ActionRequestHandler.swift @@ -13,7 +13,7 @@ import Models import Network // Sample code was sending this from a thread to another, let asume @Sendable for this -extension NSExtensionContext: @unchecked Sendable {} +extension NSExtensionContext: @unchecked @retroactive Sendable {} final class ActionRequestHandler: NSObject, NSExtensionRequestHandling, Sendable { enum Error: Swift.Error { diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index c431b169..a6054ad6 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -1211,7 +1211,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Debug; @@ -1246,7 +1246,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,7"; VALIDATE_PRODUCT = YES; }; @@ -1269,7 +1269,7 @@ INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 17.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1283,7 +1283,7 @@ SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1305,7 +1305,7 @@ INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 17.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1318,7 +1318,7 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -1353,7 +1353,7 @@ SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1387,7 +1387,7 @@ SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -1456,6 +1456,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = ""; }; name = Debug; }; @@ -1515,6 +1516,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = ""; }; name = Release; }; @@ -1569,8 +1571,19 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; + SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; + SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES; + SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES; + SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES; + SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES; + SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; + SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; + SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,7"; + _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = NO; }; name = Debug; }; @@ -1625,8 +1638,19 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; + SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; + SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES; + SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES; + SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES; + SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES; + SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; + SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; + SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,7"; + _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = NO; }; name = Release; }; @@ -1660,7 +1684,7 @@ SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1695,7 +1719,7 @@ SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; diff --git a/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesNotifications.xcscheme b/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesNotifications.xcscheme index c53de172..06441acb 100644 --- a/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesNotifications.xcscheme +++ b/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesNotifications.xcscheme @@ -56,8 +56,12 @@ debugServiceExtension = "internal" allowLocationSimulation = "YES" launchAutomaticallySubstyle = "2"> - + + + - + ) {} } -extension URL: Identifiable { +extension URL: @retroactive Identifiable { public var id: String { absoluteString } diff --git a/IceCubesApp/App/Main/AppView.swift b/IceCubesApp/App/Main/AppView.swift index c1e4cb43..95e4f910 100644 --- a/IceCubesApp/App/Main/AppView.swift +++ b/IceCubesApp/App/Main/AppView.swift @@ -21,10 +21,9 @@ struct AppView: View { @Environment(\.openWindow) var openWindow @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @Binding var selectedTab: Tab + @Binding var selectedTab: AppTab @Binding var appRouterPath: RouterPath - @State var popToRootTab: Tab = .other @State var iosTabs = iOSTabs.shared @State var sidebarTabs = SidebarTabs.shared @@ -40,45 +39,27 @@ struct AppView: View { #endif } - var availableTabs: [Tab] { + var availableTabs: [AppTab] { guard appAccountsManager.currentClient.isAuth else { - return Tab.loggedOutTab() + return AppTab.loggedOutTab() } if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact { return iosTabs.tabs } else if UIDevice.current.userInterfaceIdiom == .vision { - return Tab.visionOSTab() + return AppTab.visionOSTab() } return sidebarTabs.tabs.map { $0.tab } } + @ViewBuilder var tabBarView: some View { TabView(selection: .init(get: { selectedTab }, set: { newTab in - if newTab == .post { - #if os(visionOS) - openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)) - #else - appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) - #endif - return - } - if newTab == selectedTab { - /// Stupid hack to trigger onChange binding in tab views. - popToRootTab = .other - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - popToRootTab = selectedTab - } - } - - HapticManager.shared.fireHaptic(.tabSelection) - SoundEffectManager.shared.playSound(.tabSelection) - - selectedTab = newTab + updateTab(with: newTab) })) { ForEach(availableTabs) { tab in - tab.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab) + tab.makeContentView(selectedTab: $selectedTab) .tabItem { if userPreferences.showiPhoneTabLabel { tab.label @@ -95,8 +76,24 @@ struct AppView: View { .id(appAccountsManager.currentClient.id) .withSheetDestinations(sheetDestinations: $appRouterPath.presentedSheet) } + + private func updateTab(with newTab: AppTab) { + if newTab == .post { + #if os(visionOS) + openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)) + #else + appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) + #endif + return + } + + HapticManager.shared.fireHaptic(.tabSelection) + SoundEffectManager.shared.playSound(.tabSelection) - private func badgeFor(tab: Tab) -> Int { + selectedTab = newTab + } + + private func badgeFor(tab: AppTab) -> Int { if tab == .notifications, selectedTab != tab, let token = appAccountsManager.currentAccount.oauthToken { @@ -108,25 +105,17 @@ struct AppView: View { #if !os(visionOS) var sidebarView: some View { SideBarView(selectedTab: $selectedTab, - popToRootTab: $popToRootTab, tabs: availableTabs) { HStack(spacing: 0) { - TabView(selection: $selectedTab) { - ForEach(availableTabs) { tab in - tab - .makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab) - .tabItem { - tab.label - } - .tag(tab) + if #available(iOS 18.0, *) { + baseTabView + .tabViewStyle(.sidebarAdaptable) + .introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in + tabview.sidebar.isHidden = true } - } - .id(availableTabs.count) /// Resets the TabView state when the number of tabs changes to avoid navigation bar issues and prevent crashes - .introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in - tabview.tabBar.isHidden = horizontalSizeClass == .regular - tabview.customizableViewControllers = [] - tabview.moreNavigationController.isNavigationBarHidden = true + } else { + baseTabView } if horizontalSizeClass == .regular, appAccountsManager.currentClient.isAuth, @@ -140,10 +129,30 @@ struct AppView: View { .environment(appRouterPath) } #endif + + private var baseTabView: some View { + TabView(selection: $selectedTab) { + ForEach(availableTabs) { tab in + tab + .makeContentView(selectedTab: $selectedTab) + .toolbar(horizontalSizeClass == .regular ? .hidden : .visible, for: .tabBar) + .tabItem { + tab.label + } + .tag(tab) + } + } + .id(availableTabs.count) /// Resets the TabView state when the number of tabs changes to avoid navigation bar issues and prevent crashes + .introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in + tabview.tabBar.isHidden = horizontalSizeClass == .regular + tabview.customizableViewControllers = [] + tabview.moreNavigationController.isNavigationBarHidden = true + } + } var notificationsSecondaryColumn: some View { - NotificationsTab(selectedTab: .constant(.notifications), - popToRootTab: $popToRootTab, lockedType: nil) + NotificationsTab(selectedTab: .constant(.notifications) + , lockedType: nil) .environment(\.isSecondaryColumn, true) .frame(maxWidth: .secondaryColumnWidth) .id(appAccountsManager.currentAccount.id) diff --git a/IceCubesApp/App/Main/IceCubesApp.swift b/IceCubesApp/App/Main/IceCubesApp.swift index 231919f7..02b5208c 100644 --- a/IceCubesApp/App/Main/IceCubesApp.swift +++ b/IceCubesApp/App/Main/IceCubesApp.swift @@ -29,7 +29,7 @@ struct IceCubesApp: App { @State var quickLook = QuickLook.shared @State var theme = Theme.shared - @State var selectedTab: Tab = .timeline + @State var selectedTab: AppTab = .timeline @State var appRouterPath = RouterPath() @State var isSupporter: Bool = false diff --git a/IceCubesApp/App/SideBarView.swift b/IceCubesApp/App/SideBarView.swift index 045c3d08..ae7a9ce7 100644 --- a/IceCubesApp/App/SideBarView.swift +++ b/IceCubesApp/App/SideBarView.swift @@ -18,14 +18,13 @@ struct SideBarView: View { @Environment(UserPreferences.self) private var userPreferences @Environment(RouterPath.self) private var routerPath - @Binding var selectedTab: Tab - @Binding var popToRootTab: Tab - var tabs: [Tab] + @Binding var selectedTab: AppTab + var tabs: [AppTab] @ViewBuilder var content: () -> Content @State private var sidebarTabs = SidebarTabs.shared - private func badgeFor(tab: Tab) -> Int { + private func badgeFor(tab: AppTab) -> Int { if tab == .notifications, selectedTab != tab, let token = appAccounts.currentAccount.oauthToken { @@ -34,7 +33,7 @@ struct SideBarView: View { return 0 } - private func makeIconForTab(tab: Tab) -> some View { + private func makeIconForTab(tab: AppTab) -> some View { HStack { ZStack(alignment: .topTrailing) { SideBarIcon(systemIconName: tab.iconName, @@ -89,7 +88,7 @@ struct SideBarView: View { .offset(x: 2, y: -2) } .buttonStyle(.borderedProminent) - .help(Tab.post.title) + .help(AppTab.post.title) } private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View { @@ -139,7 +138,7 @@ struct SideBarView: View { if let accountName { "tab.profile-account-\(accountName)" } else { - Tab.profile.title + AppTab.profile.title } } @@ -149,13 +148,6 @@ struct SideBarView: View { Button { // ensure keyboard is always dismissed when selecting a tab hideKeyboard() - - if tab == selectedTab { - popToRootTab = .other - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - popToRootTab = tab - } - } selectedTab = tab SoundEffectManager.shared.playSound(.tabSelection) if tab == .notifications { diff --git a/IceCubesApp/App/Tabs/ExploreTab.swift b/IceCubesApp/App/Tabs/ExploreTab.swift index 5b2cce1c..cac3cb2f 100644 --- a/IceCubesApp/App/Tabs/ExploreTab.swift +++ b/IceCubesApp/App/Tabs/ExploreTab.swift @@ -13,12 +13,10 @@ struct ExploreTab: View { @Environment(CurrentAccount.self) private var currentAccount @Environment(Client.self) private var client @State private var routerPath = RouterPath() - @State private var scrollToTopSignal: Int = 0 - @Binding var popToRootTab: Tab var body: some View { NavigationStack(path: $routerPath.path) { - ExploreView(scrollToTopSignal: $scrollToTopSignal) + ExploreView() .withAppRouter() .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar) @@ -28,15 +26,6 @@ struct ExploreTab: View { } .withSafariRouter() .environment(routerPath) - .onChange(of: $popToRootTab.wrappedValue) { _, newValue in - if newValue == .explore { - if routerPath.path.isEmpty { - scrollToTopSignal += 1 - } else { - routerPath.path = [] - } - } - } .onChange(of: client.id) { routerPath.path = [] } diff --git a/IceCubesApp/App/Tabs/MessagesTab.swift b/IceCubesApp/App/Tabs/MessagesTab.swift index d265b73d..bf8f01f4 100644 --- a/IceCubesApp/App/Tabs/MessagesTab.swift +++ b/IceCubesApp/App/Tabs/MessagesTab.swift @@ -15,12 +15,10 @@ struct MessagesTab: View { @Environment(CurrentAccount.self) private var currentAccount @Environment(AppAccountsManager.self) private var appAccount @State private var routerPath = RouterPath() - @State private var scrollToTopSignal: Int = 0 - @Binding var popToRootTab: Tab var body: some View { NavigationStack(path: $routerPath.path) { - ConversationsListView(scrollToTopSignal: $scrollToTopSignal) + ConversationsListView() .withAppRouter() .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) .toolbar { @@ -29,15 +27,6 @@ struct MessagesTab: View { .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar) .id(client.id) } - .onChange(of: $popToRootTab.wrappedValue) { _, newValue in - if newValue == .messages { - if routerPath.path.isEmpty { - scrollToTopSignal += 1 - } else { - routerPath.path = [] - } - } - } .onChange(of: client.id) { routerPath.path = [] } diff --git a/IceCubesApp/App/Tabs/NotificationTab.swift b/IceCubesApp/App/Tabs/NotificationTab.swift index 60713ee1..963cad49 100644 --- a/IceCubesApp/App/Tabs/NotificationTab.swift +++ b/IceCubesApp/App/Tabs/NotificationTab.swift @@ -20,16 +20,14 @@ struct NotificationsTab: View { @Environment(UserPreferences.self) private var userPreferences @Environment(PushNotificationsService.self) private var pushNotificationsService @State private var routerPath = RouterPath() - @State private var scrollToTopSignal: Int = 0 - @Binding var selectedTab: Tab - @Binding var popToRootTab: Tab + @Binding var selectedTab: AppTab let lockedType: Models.Notification.NotificationType? var body: some View { NavigationStack(path: $routerPath.path) { - NotificationsListView(lockedType: lockedType, scrollToTopSignal: $scrollToTopSignal) + NotificationsListView(lockedType: lockedType) .withAppRouter() .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) .toolbar { @@ -51,15 +49,6 @@ struct NotificationsTab: View { } .withSafariRouter() .environment(routerPath) - .onChange(of: $popToRootTab.wrappedValue) { _, newValue in - if newValue == .notifications { - if routerPath.path.isEmpty { - scrollToTopSignal += 1 - } else { - routerPath.path = [] - } - } - } .onChange(of: selectedTab) { _, _ in clearNotifications() } @@ -92,10 +81,12 @@ struct NotificationsTab: View { private func clearNotifications() { if selectedTab == .notifications || isSecondaryColumn { - if let token = appAccount.currentAccount.oauthToken { + if let token = appAccount.currentAccount.oauthToken, userPreferences.notificationsCount[token] ?? 0 > 0 { userPreferences.notificationsCount[token] = 0 } - watcher.unreadNotificationsCount = 0 + if watcher.unreadNotificationsCount > 0 { + watcher.unreadNotificationsCount = 0 + } } } } diff --git a/IceCubesApp/App/Tabs/ProfileTab.swift b/IceCubesApp/App/Tabs/ProfileTab.swift index 22a80747..c63e7836 100644 --- a/IceCubesApp/App/Tabs/ProfileTab.swift +++ b/IceCubesApp/App/Tabs/ProfileTab.swift @@ -14,32 +14,21 @@ struct ProfileTab: View { @Environment(Client.self) private var client @Environment(CurrentAccount.self) private var currentAccount @State private var routerPath = RouterPath() - @State private var scrollToTopSignal: Int = 0 - @Binding var popToRootTab: Tab var body: some View { NavigationStack(path: $routerPath.path) { if let account = currentAccount.account { - AccountDetailView(account: account, scrollToTopSignal: $scrollToTopSignal) + AccountDetailView(account: account) .withAppRouter() .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar) .id(account.id) } else { - AccountDetailView(account: .placeholder(), scrollToTopSignal: $scrollToTopSignal) + AccountDetailView(account: .placeholder()) .redacted(reason: .placeholder) .allowsHitTesting(false) } } - .onChange(of: $popToRootTab.wrappedValue) { _, newValue in - if newValue == .profile { - if routerPath.path.isEmpty { - scrollToTopSignal += 1 - } else { - routerPath.path = [] - } - } - } .onChange(of: client.id) { routerPath.path = [] } diff --git a/IceCubesApp/App/Tabs/Settings/AboutView.swift b/IceCubesApp/App/Tabs/Settings/AboutView.swift index 413055ce..79e5eb8a 100644 --- a/IceCubesApp/App/Tabs/Settings/AboutView.swift +++ b/IceCubesApp/App/Tabs/Settings/AboutView.swift @@ -152,13 +152,13 @@ struct AboutView: View { private func fetchAccounts() async { await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - let viewModel = try await fetchAccountViewModel(account: "dimillian@mastodon.social") + let viewModel = try await fetchAccountViewModel(client, account: "dimillian@mastodon.social") await MainActor.run { dimillianAccount = viewModel } } group.addTask { - let viewModel = try await fetchAccountViewModel(account: "icecubesapp@mastodon.online") + let viewModel = try await fetchAccountViewModel(client, account: "icecubesapp@mastodon.online") await MainActor.run { iceCubesAccount = viewModel } @@ -166,7 +166,7 @@ struct AboutView: View { } } - private func fetchAccountViewModel(account: String) async throws -> AccountsListRowViewModel { + private func fetchAccountViewModel(_ client: Client, account: String) async throws -> AccountsListRowViewModel { let dimillianAccount: Account = try await client.get(endpoint: Accounts.lookup(name: account)) let rel: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [dimillianAccount.id])) return .init(account: dimillianAccount, relationShip: rel.first) diff --git a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift index 741be139..3b8ebf22 100644 --- a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift +++ b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift @@ -92,6 +92,7 @@ struct AddAccountView: View { } .onAppear { isInstanceURLFieldFocused = true + let instanceName = instanceName Task { let instances = await instanceSocialClient.fetchInstances(keyword: instanceName) withAnimation { @@ -102,6 +103,8 @@ struct AddAccountView: View { } .onChange(of: instanceName) { searchingTask.cancel() + let instanceName = instanceName + let instanceSocialClient = instanceSocialClient searchingTask = Task { try? await Task.sleep(for: .seconds(0.1)) guard !Task.isCancelled else { return } @@ -127,7 +130,7 @@ struct AddAccountView: View { let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance) withAnimation { self.instance = instance - instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box + self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box } instanceFetchError = nil } else { diff --git a/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift b/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift index b83073ff..2feb11eb 100644 --- a/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift +++ b/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift @@ -33,6 +33,10 @@ struct IconSelectorView: View { var appIconName: String { return "AppIconAlternate\(rawValue)" } + + var previewImageName: String { + return "AppIconAlternate\(rawValue)-image" + } } struct IconSelector: Identifiable { @@ -98,7 +102,7 @@ struct IconSelectorView: View { } } label: { ZStack(alignment: .bottomTrailing) { - Image(uiImage: .init(named: icon.appIconName) ?? .init()) + Image(icon.previewImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(minHeight: 125, maxHeight: 1024) diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index 18ee2a05..6a49b037 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -28,8 +28,6 @@ struct SettingsTabs: View { @State private var cachedRemoved = false @State private var timelineCache = TimelineCache() - @Binding var popToRootTab: Tab - let isModal: Bool @State private var startingPoint: SettingsStartingPoint? = nil @@ -103,11 +101,6 @@ struct SettingsTabs: View { } .withSafariRouter() .environment(routerPath) - .onChange(of: $popToRootTab.wrappedValue) { _, newValue in - if newValue == .notifications { - routerPath.path = [] - } - } } private var accountsSection: some View { @@ -265,7 +258,7 @@ struct SettingsTabs: View { Text("settings.app.icon") } icon: { let icon = IconSelectorView.Icon(string: UIApplication.shared.alternateIconName ?? "AppIcon") - if let image: UIImage = .init(named: icon.appIconName) { + if let image: UIImage = .init(named: icon.previewImageName) { Image(uiImage: image) .resizable() .frame(width: 25, height: 25) diff --git a/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift index f5911a00..13099f19 100644 --- a/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift @@ -14,35 +14,35 @@ struct TabbarEntriesSettingsView: View { Form { Section { Picker("settings.tabs.first-tab", selection: $tabs.firstTab) { - ForEach(Tab.allCases) { tab in + ForEach(AppTab.allCases) { tab in if tab == tabs.firstTab || !tabs.tabs.contains(tab) { tab.label.tag(tab) } } } Picker("settings.tabs.second-tab", selection: $tabs.secondTab) { - ForEach(Tab.allCases) { tab in + ForEach(AppTab.allCases) { tab in if tab == tabs.secondTab || !tabs.tabs.contains(tab) { tab.label.tag(tab) } } } Picker("settings.tabs.third-tab", selection: $tabs.thirdTab) { - ForEach(Tab.allCases) { tab in + ForEach(AppTab.allCases) { tab in if tab == tabs.thirdTab || !tabs.tabs.contains(tab) { tab.label.tag(tab) } } } Picker("settings.tabs.fourth-tab", selection: $tabs.fourthTab) { - ForEach(Tab.allCases) { tab in + ForEach(AppTab.allCases) { tab in if tab == tabs.fourthTab || !tabs.tabs.contains(tab) { tab.label.tag(tab) } } } Picker("settings.tabs.fifth-tab", selection: $tabs.fifthTab) { - ForEach(Tab.allCases) { tab in + ForEach(AppTab.allCases) { tab in if tab == tabs.fifthTab || !tabs.tabs.contains(tab) { tab.label.tag(tab) } diff --git a/IceCubesApp/App/Tabs/Tabs.swift b/IceCubesApp/App/Tabs/Tabs.swift index 7e6ff76c..ec41be53 100644 --- a/IceCubesApp/App/Tabs/Tabs.swift +++ b/IceCubesApp/App/Tabs/Tabs.swift @@ -7,7 +7,7 @@ import StatusKit import SwiftUI @MainActor -enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable { +enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable { case timeline, notifications, mentions, explore, messages, settings, other case trending, federated, local case profile @@ -22,37 +22,37 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable { rawValue } - static func loggedOutTab() -> [Tab] { + static func loggedOutTab() -> [AppTab] { [.timeline, .settings] } - static func visionOSTab() -> [Tab] { + static func visionOSTab() -> [AppTab] { [.profile, .timeline, .notifications, .mentions, .explore, .post, .settings] } @ViewBuilder - func makeContentView(selectedTab: Binding, popToRootTab: Binding) -> some View { + func makeContentView(selectedTab: Binding) -> some View { switch self { case .timeline: - TimelineTab(popToRootTab: popToRootTab) + TimelineTab() case .trending: - TimelineTab(popToRootTab: popToRootTab, timeline: .trending) + TimelineTab(timeline: .trending) case .local: - TimelineTab(popToRootTab: popToRootTab, timeline: .local) + TimelineTab(timeline: .local) case .federated: - TimelineTab(popToRootTab: popToRootTab, timeline: .federated) + TimelineTab(timeline: .federated) case .notifications: - NotificationsTab(selectedTab: selectedTab, popToRootTab: popToRootTab, lockedType: nil) + NotificationsTab(selectedTab: selectedTab, lockedType: nil) case .mentions: - NotificationsTab(selectedTab: selectedTab, popToRootTab: popToRootTab, lockedType: .mention) + NotificationsTab(selectedTab: selectedTab, lockedType: .mention) case .explore: - ExploreTab(popToRootTab: popToRootTab) + ExploreTab() case .messages: - MessagesTab(popToRootTab: popToRootTab) + MessagesTab() case .settings: - SettingsTabs(popToRootTab: popToRootTab, isModal: false) + SettingsTabs(isModal: false) case .profile: - ProfileTab(popToRootTab: popToRootTab) + ProfileTab() case .bookmarks: NavigationTab { AccountStatusesListView(mode: .bookmarks) @@ -168,7 +168,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable { @Observable class SidebarTabs { struct SidedebarTab: Hashable, Codable { - let tab: Tab + let tab: AppTab var enabled: Bool } @@ -202,7 +202,7 @@ class SidebarTabs { } } - func isEnabled(_ tab: Tab) -> Bool { + func isEnabled(_ tab: AppTab) -> Bool { tabs.first(where: { $0.tab.id == tab.id })?.enabled == true } @@ -219,45 +219,45 @@ class iOSTabs { } class Storage { - @AppStorage(TabEntries.first.rawValue) var firstTab = Tab.timeline - @AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications - @AppStorage(TabEntries.third.rawValue) var thirdTab = Tab.explore - @AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.links - @AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile + @AppStorage(TabEntries.first.rawValue) var firstTab = AppTab.timeline + @AppStorage(TabEntries.second.rawValue) var secondTab = AppTab.notifications + @AppStorage(TabEntries.third.rawValue) var thirdTab = AppTab.explore + @AppStorage(TabEntries.fourth.rawValue) var fourthTab = AppTab.links + @AppStorage(TabEntries.fifth.rawValue) var fifthTab = AppTab.profile } private let storage = Storage() public static let shared = iOSTabs() - var tabs: [Tab] { + var tabs: [AppTab] { [firstTab, secondTab, thirdTab, fourthTab, fifthTab] } - var firstTab: Tab { + var firstTab: AppTab { didSet { storage.firstTab = firstTab } } - var secondTab: Tab { + var secondTab: AppTab { didSet { storage.secondTab = secondTab } } - var thirdTab: Tab { + var thirdTab: AppTab { didSet { storage.thirdTab = thirdTab } } - var fourthTab: Tab { + var fourthTab: AppTab { didSet { storage.fourthTab = fourthTab } } - var fifthTab: Tab { + var fifthTab: AppTab { didSet { storage.fifthTab = fifthTab } diff --git a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift index ef1a9429..f34d40d5 100644 --- a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift +++ b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift @@ -77,6 +77,7 @@ struct AddRemoteTimelineView: View { .onAppear { isInstanceURLFieldFocused = true let client = InstanceSocialClient() + let instanceName = instanceName Task { instances = await client.fetchInstances(keyword: instanceName) } diff --git a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift index b6989128..6cc8a800 100644 --- a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift @@ -18,12 +18,10 @@ struct TimelineTab: View { @Environment(UserPreferences.self) private var preferences @Environment(Client.self) private var client @State private var routerPath = RouterPath() - @Binding var popToRootTab: Tab @State private var didAppear: Bool = false @State private var timeline: TimelineFilter = .home @State private var selectedTagGroup: TagGroup? - @State private var scrollToTopSignal: Int = 0 @Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline] @Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup] @@ -33,9 +31,8 @@ struct TimelineTab: View { private let canFilterTimeline: Bool - init(popToRootTab: Binding, timeline: TimelineFilter? = nil) { + init(timeline: TimelineFilter? = nil) { canFilterTimeline = timeline == nil - _popToRootTab = popToRootTab _timeline = .init(initialValue: timeline ?? .home) } @@ -44,7 +41,6 @@ struct TimelineTab: View { TimelineView(timeline: $timeline, pinnedFilters: $pinnedFilters, selectedTagGroup: $selectedTagGroup, - scrollToTopSignal: $scrollToTopSignal, canFilterTimeline: canFilterTimeline) .withAppRouter() .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) @@ -77,15 +73,6 @@ struct TimelineTab: View { .onChange(of: currentAccount.account?.id) { resetTimelineFilter() } - .onChange(of: $popToRootTab.wrappedValue) { _, newValue in - if newValue == .timeline { - if routerPath.path.isEmpty { - scrollToTopSignal += 1 - } else { - routerPath.path = [] - } - } - } .onChange(of: client.id) { routerPath.path = [] } diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate0-image.imageset/1024.png b/IceCubesApp/Assets.xcassets/AppIconAlternate0-image.imageset/1024.png new file mode 100644 index 00000000..97621cb4 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate0-image.imageset/1024.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate0-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate0-image.imageset/Contents.json new file mode 100644 index 00000000..94202f55 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate0-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "1024.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate1-image.imageset/1024.png b/IceCubesApp/Assets.xcassets/AppIconAlternate1-image.imageset/1024.png new file mode 100644 index 00000000..8c326b21 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate1-image.imageset/1024.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate1-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate1-image.imageset/Contents.json new file mode 100644 index 00000000..94202f55 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate1-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "1024.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate10-image.imageset/AppIconAlternate10.png b/IceCubesApp/Assets.xcassets/AppIconAlternate10-image.imageset/AppIconAlternate10.png new file mode 100644 index 00000000..dbf5fc1e Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate10-image.imageset/AppIconAlternate10.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate10-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate10-image.imageset/Contents.json new file mode 100644 index 00000000..64c67187 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate10-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AppIconAlternate10.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate11-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate11-image.imageset/Contents.json new file mode 100644 index 00000000..8da33fcc --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate11-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon15.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate11-image.imageset/icon15.png b/IceCubesApp/Assets.xcassets/AppIconAlternate11-image.imageset/icon15.png new file mode 100644 index 00000000..ca28716b Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate11-image.imageset/icon15.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate12-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate12-image.imageset/Contents.json new file mode 100644 index 00000000..fa51eb45 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate12-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon16.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate12-image.imageset/icon16.png b/IceCubesApp/Assets.xcassets/AppIconAlternate12-image.imageset/icon16.png new file mode 100644 index 00000000..a28c6064 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate12-image.imageset/icon16.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate13-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate13-image.imageset/Contents.json new file mode 100644 index 00000000..1524f022 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate13-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon17.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate13-image.imageset/icon17.png b/IceCubesApp/Assets.xcassets/AppIconAlternate13-image.imageset/icon17.png new file mode 100644 index 00000000..566b6bca Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate13-image.imageset/icon17.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate14-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate14-image.imageset/Contents.json new file mode 100644 index 00000000..88474690 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate14-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon18.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate14-image.imageset/icon18.png b/IceCubesApp/Assets.xcassets/AppIconAlternate14-image.imageset/icon18.png new file mode 100644 index 00000000..dd548215 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate14-image.imageset/icon18.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate15-image.imageset/AppIconAlternate15.png b/IceCubesApp/Assets.xcassets/AppIconAlternate15-image.imageset/AppIconAlternate15.png new file mode 100644 index 00000000..392ad757 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate15-image.imageset/AppIconAlternate15.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate15-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate15-image.imageset/Contents.json new file mode 100644 index 00000000..6d546d92 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate15-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AppIconAlternate15.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate16-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate16-image.imageset/Contents.json new file mode 100644 index 00000000..2945b36b --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate16-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate16-image.imageset/icon.png b/IceCubesApp/Assets.xcassets/AppIconAlternate16-image.imageset/icon.png new file mode 100644 index 00000000..d60b9cc5 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate16-image.imageset/icon.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate17-image.imageset/Alternate43-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate17-image.imageset/Alternate43-fs8.png new file mode 100644 index 00000000..bea14b9f Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate17-image.imageset/Alternate43-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate17-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate17-image.imageset/Contents.json new file mode 100644 index 00000000..35c6aa09 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate17-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Alternate43-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate18-image.imageset/Alternate44-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate18-image.imageset/Alternate44-fs8.png new file mode 100644 index 00000000..ad901af5 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate18-image.imageset/Alternate44-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate18-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate18-image.imageset/Contents.json new file mode 100644 index 00000000..717c0f1e --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate18-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Alternate44-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate19-image.imageset/Alternate45-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate19-image.imageset/Alternate45-fs8.png new file mode 100644 index 00000000..d316bd7b Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate19-image.imageset/Alternate45-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate19-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate19-image.imageset/Contents.json new file mode 100644 index 00000000..08ddaee5 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate19-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Alternate45-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate2-image.imageset/AppIconAlternate2.png b/IceCubesApp/Assets.xcassets/AppIconAlternate2-image.imageset/AppIconAlternate2.png new file mode 100644 index 00000000..9f2f7980 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate2-image.imageset/AppIconAlternate2.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate2-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate2-image.imageset/Contents.json new file mode 100644 index 00000000..e3ab058b --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate2-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AppIconAlternate2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate20-image.imageset/Alternate46-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate20-image.imageset/Alternate46-fs8.png new file mode 100644 index 00000000..77263f0d Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate20-image.imageset/Alternate46-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate20-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate20-image.imageset/Contents.json new file mode 100644 index 00000000..5794a7a9 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate20-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Alternate46-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate21-image.imageset/Alternate47-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate21-image.imageset/Alternate47-fs8.png new file mode 100644 index 00000000..377a8b6c Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate21-image.imageset/Alternate47-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate21-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate21-image.imageset/Contents.json new file mode 100644 index 00000000..59f28bba --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate21-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Alternate47-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate22-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate22-image.imageset/Contents.json new file mode 100644 index 00000000..2ddb7653 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate22-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon23.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate22-image.imageset/icon23.png b/IceCubesApp/Assets.xcassets/AppIconAlternate22-image.imageset/icon23.png new file mode 100644 index 00000000..4cb9da3b Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate22-image.imageset/icon23.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate23-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate23-image.imageset/Contents.json new file mode 100644 index 00000000..4c6a2000 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate23-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate23-image.imageset/icon24.png b/IceCubesApp/Assets.xcassets/AppIconAlternate23-image.imageset/icon24.png new file mode 100644 index 00000000..0580d5c5 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate23-image.imageset/icon24.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate24-image.imageset/AppIconAlternate21.png b/IceCubesApp/Assets.xcassets/AppIconAlternate24-image.imageset/AppIconAlternate21.png new file mode 100644 index 00000000..1234e159 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate24-image.imageset/AppIconAlternate21.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate24-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate24-image.imageset/Contents.json new file mode 100644 index 00000000..50c1ee26 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate24-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AppIconAlternate21.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate25-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate25-image.imageset/Contents.json new file mode 100644 index 00000000..9e6ce966 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate25-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon22.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate25-image.imageset/icon22.png b/IceCubesApp/Assets.xcassets/AppIconAlternate25-image.imageset/icon22.png new file mode 100644 index 00000000..d2b09fc0 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate25-image.imageset/icon22.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate26-image.imageset/AppIconAlternate20.png b/IceCubesApp/Assets.xcassets/AppIconAlternate26-image.imageset/AppIconAlternate20.png new file mode 100644 index 00000000..594e29be Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate26-image.imageset/AppIconAlternate20.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate26-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate26-image.imageset/Contents.json new file mode 100644 index 00000000..fecaf514 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate26-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AppIconAlternate20.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate27-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate27-image.imageset/Contents.json new file mode 100644 index 00000000..b30d485e --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate27-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icecubes_comic_01.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate27-image.imageset/icecubes_comic_01.png b/IceCubesApp/Assets.xcassets/AppIconAlternate27-image.imageset/icecubes_comic_01.png new file mode 100644 index 00000000..365d55d4 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate27-image.imageset/icecubes_comic_01.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate28-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate28-image.imageset/Contents.json new file mode 100644 index 00000000..3d368482 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate28-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icecubes_comic_02.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate28-image.imageset/icecubes_comic_02.png b/IceCubesApp/Assets.xcassets/AppIconAlternate28-image.imageset/icecubes_comic_02.png new file mode 100644 index 00000000..92ffdf8d Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate28-image.imageset/icecubes_comic_02.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate29-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate29-image.imageset/Contents.json new file mode 100644 index 00000000..6ed16ffb --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate29-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icecubes_comic_03.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate29-image.imageset/icecubes_comic_03.png b/IceCubesApp/Assets.xcassets/AppIconAlternate29-image.imageset/icecubes_comic_03.png new file mode 100644 index 00000000..680edef3 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate29-image.imageset/icecubes_comic_03.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate3-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate3-image.imageset/Contents.json new file mode 100644 index 00000000..7878a00d --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate3-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon6.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate3-image.imageset/icon6.png b/IceCubesApp/Assets.xcassets/AppIconAlternate3-image.imageset/icon6.png new file mode 100644 index 00000000..02820915 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate3-image.imageset/icon6.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate30-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate30-image.imageset/Contents.json new file mode 100644 index 00000000..ecd0aa48 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate30-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "blue.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate30-image.imageset/blue.png b/IceCubesApp/Assets.xcassets/AppIconAlternate30-image.imageset/blue.png new file mode 100644 index 00000000..3607e495 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate30-image.imageset/blue.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate31-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate31-image.imageset/Contents.json new file mode 100644 index 00000000..92bbe2c5 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate31-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "purple_alt.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate31-image.imageset/purple_alt.png b/IceCubesApp/Assets.xcassets/AppIconAlternate31-image.imageset/purple_alt.png new file mode 100644 index 00000000..aeb9aa17 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate31-image.imageset/purple_alt.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate32-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate32-image.imageset/Contents.json new file mode 100644 index 00000000..8e6290eb --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate32-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "purple.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate32-image.imageset/purple.png b/IceCubesApp/Assets.xcassets/AppIconAlternate32-image.imageset/purple.png new file mode 100644 index 00000000..b91cc908 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate32-image.imageset/purple.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate33-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate33-image.imageset/Contents.json new file mode 100644 index 00000000..901193ba --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate33-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "stripe.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate33-image.imageset/stripe.png b/IceCubesApp/Assets.xcassets/AppIconAlternate33-image.imageset/stripe.png new file mode 100644 index 00000000..8f22e705 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate33-image.imageset/stripe.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate34-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate34-image.imageset/Contents.json new file mode 100644 index 00000000..1c714b8e --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate34-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "blue_alt2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate34-image.imageset/blue_alt2.png b/IceCubesApp/Assets.xcassets/AppIconAlternate34-image.imageset/blue_alt2.png new file mode 100644 index 00000000..5f943c14 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate34-image.imageset/blue_alt2.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate35-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate35-image.imageset/Contents.json new file mode 100644 index 00000000..297a0ab6 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate35-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "purple_alt2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate35-image.imageset/purple_alt2.png b/IceCubesApp/Assets.xcassets/AppIconAlternate35-image.imageset/purple_alt2.png new file mode 100644 index 00000000..ca202ad6 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate35-image.imageset/purple_alt2.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate36-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate36-image.imageset/Contents.json new file mode 100644 index 00000000..fa25a7d7 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate36-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "stripe_alt.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate36-image.imageset/stripe_alt.png b/IceCubesApp/Assets.xcassets/AppIconAlternate36-image.imageset/stripe_alt.png new file mode 100644 index 00000000..56e5ec99 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate36-image.imageset/stripe_alt.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate37-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate37-image.imageset/Contents.json new file mode 100644 index 00000000..2945b36b --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate37-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate37-image.imageset/icon.png b/IceCubesApp/Assets.xcassets/AppIconAlternate37-image.imageset/icon.png new file mode 100644 index 00000000..3d20a345 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate37-image.imageset/icon.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate38-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate38-image.imageset/Contents.json new file mode 100644 index 00000000..2945b36b --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate38-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate38-image.imageset/icon.png b/IceCubesApp/Assets.xcassets/AppIconAlternate38-image.imageset/icon.png new file mode 100644 index 00000000..e885bcf7 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate38-image.imageset/icon.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate39-image.imageset/Alternate-48-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate39-image.imageset/Alternate-48-fs8.png new file mode 100644 index 00000000..50846a3d Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate39-image.imageset/Alternate-48-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate39-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate39-image.imageset/Contents.json new file mode 100644 index 00000000..4d9cb254 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate39-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Alternate-48-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate4-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate4-image.imageset/Contents.json new file mode 100644 index 00000000..df233923 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate4-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon7.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate4-image.imageset/icon7.png b/IceCubesApp/Assets.xcassets/AppIconAlternate4-image.imageset/icon7.png new file mode 100644 index 00000000..1f8696b7 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate4-image.imageset/icon7.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate40-image.imageset/Alternate-49-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate40-image.imageset/Alternate-49-fs8.png new file mode 100644 index 00000000..a5306e3e Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate40-image.imageset/Alternate-49-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate40-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate40-image.imageset/Contents.json new file mode 100644 index 00000000..797fc54d --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate40-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Alternate-49-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate41-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate41-image.imageset/Contents.json new file mode 100644 index 00000000..ba296495 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate41-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "e2a44f35b779f021.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate41-image.imageset/e2a44f35b779f021.png b/IceCubesApp/Assets.xcassets/AppIconAlternate41-image.imageset/e2a44f35b779f021.png new file mode 100644 index 00000000..37d9b31b Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate41-image.imageset/e2a44f35b779f021.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate42-image.imageset/4c26b880d454e2ec.png b/IceCubesApp/Assets.xcassets/AppIconAlternate42-image.imageset/4c26b880d454e2ec.png new file mode 100644 index 00000000..5703324b Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate42-image.imageset/4c26b880d454e2ec.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate42-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate42-image.imageset/Contents.json new file mode 100644 index 00000000..63cc9d8a --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate42-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "4c26b880d454e2ec.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate43-image.imageset/63245c61df096188.png b/IceCubesApp/Assets.xcassets/AppIconAlternate43-image.imageset/63245c61df096188.png new file mode 100644 index 00000000..4e46e781 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate43-image.imageset/63245c61df096188.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate43-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate43-image.imageset/Contents.json new file mode 100644 index 00000000..0e023730 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate43-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "63245c61df096188.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate44-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate44-image.imageset/Contents.json new file mode 100644 index 00000000..f4b4d511 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate44-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon38.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate44-image.imageset/icon38.png b/IceCubesApp/Assets.xcassets/AppIconAlternate44-image.imageset/icon38.png new file mode 100644 index 00000000..a8d0ea90 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate44-image.imageset/icon38.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate45-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate45-image.imageset/Contents.json new file mode 100644 index 00000000..da92f124 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate45-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon39.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate45-image.imageset/icon39.png b/IceCubesApp/Assets.xcassets/AppIconAlternate45-image.imageset/icon39.png new file mode 100644 index 00000000..d1d24d1d Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate45-image.imageset/icon39.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate46-image.imageset/AppIcon-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate46-image.imageset/AppIcon-fs8.png new file mode 100644 index 00000000..411727e5 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate46-image.imageset/AppIcon-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate46-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate46-image.imageset/Contents.json new file mode 100644 index 00000000..a4e3d7fd --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate46-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AppIcon-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate47-image.imageset/AppIconAlternate47-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate47-image.imageset/AppIconAlternate47-fs8.png new file mode 100644 index 00000000..5df70b23 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate47-image.imageset/AppIconAlternate47-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate47-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate47-image.imageset/Contents.json new file mode 100644 index 00000000..bbae176e --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate47-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AppIconAlternate47-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate48-image.imageset/AppIconAlternate48-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate48-image.imageset/AppIconAlternate48-fs8.png new file mode 100644 index 00000000..82f17472 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate48-image.imageset/AppIconAlternate48-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate48-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate48-image.imageset/Contents.json new file mode 100644 index 00000000..47e31185 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate48-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AppIconAlternate48-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate49-image.imageset/AppIconAlternate49-fs8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate49-image.imageset/AppIconAlternate49-fs8.png new file mode 100644 index 00000000..f37a1d28 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate49-image.imageset/AppIconAlternate49-fs8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate49-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate49-image.imageset/Contents.json new file mode 100644 index 00000000..ab089d51 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate49-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AppIconAlternate49-fs8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate5-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate5-image.imageset/Contents.json new file mode 100644 index 00000000..c89a778e --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate5-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Icon8.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate5-image.imageset/Icon8.png b/IceCubesApp/Assets.xcassets/AppIconAlternate5-image.imageset/Icon8.png new file mode 100644 index 00000000..e37cdc66 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate5-image.imageset/Icon8.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate6-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate6-image.imageset/Contents.json new file mode 100644 index 00000000..033b7a16 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate6-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_4b25c3a5-7ae2-401b-9b2f-17c9832e175a.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate6-image.imageset/Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_4b25c3a5-7ae2-401b-9b2f-17c9832e175a.png b/IceCubesApp/Assets.xcassets/AppIconAlternate6-image.imageset/Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_4b25c3a5-7ae2-401b-9b2f-17c9832e175a.png new file mode 100644 index 00000000..89cdb251 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate6-image.imageset/Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_4b25c3a5-7ae2-401b-9b2f-17c9832e175a.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate7-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate7-image.imageset/Contents.json new file mode 100644 index 00000000..17d9026d --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate7-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_7ca60288-3e96-43a3-a88b-f4c882ac2183.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate7-image.imageset/Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_7ca60288-3e96-43a3-a88b-f4c882ac2183.png b/IceCubesApp/Assets.xcassets/AppIconAlternate7-image.imageset/Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_7ca60288-3e96-43a3-a88b-f4c882ac2183.png new file mode 100644 index 00000000..e62bd506 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate7-image.imageset/Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_7ca60288-3e96-43a3-a88b-f4c882ac2183.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate8-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate8-image.imageset/Contents.json new file mode 100644 index 00000000..9c10c7b9 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate8-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_2338f943-14f9-4639-aabf-70dc18cf4d52.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate8-image.imageset/Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_2338f943-14f9-4639-aabf-70dc18cf4d52.png b/IceCubesApp/Assets.xcassets/AppIconAlternate8-image.imageset/Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_2338f943-14f9-4639-aabf-70dc18cf4d52.png new file mode 100644 index 00000000..35144843 Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate8-image.imageset/Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_2338f943-14f9-4639-aabf-70dc18cf4d52.png differ diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate9-image.imageset/Contents.json b/IceCubesApp/Assets.xcassets/AppIconAlternate9-image.imageset/Contents.json new file mode 100644 index 00000000..e5416451 --- /dev/null +++ b/IceCubesApp/Assets.xcassets/AppIconAlternate9-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon13.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Assets.xcassets/AppIconAlternate9-image.imageset/icon13.png b/IceCubesApp/Assets.xcassets/AppIconAlternate9-image.imageset/icon13.png new file mode 100644 index 00000000..af7bfb4e Binary files /dev/null and b/IceCubesApp/Assets.xcassets/AppIconAlternate9-image.imageset/icon13.png differ diff --git a/IceCubesAppIntents/AppAccountEntity.swift b/IceCubesAppIntents/AppAccountEntity.swift index e6cb0f09..0797672b 100644 --- a/IceCubesAppIntents/AppAccountEntity.swift +++ b/IceCubesAppIntents/AppAccountEntity.swift @@ -5,9 +5,6 @@ import Foundation import Models import Network -extension IntentDescription: @unchecked Sendable {} -extension TypeDisplayRepresentation: @unchecked Sendable {} - public struct AppAccountEntity: Identifiable, AppEntity { public var id: String { account.id } diff --git a/IceCubesAppIntents/TabIntent.swift b/IceCubesAppIntents/TabIntent.swift index b4bb34ae..7f6fd107 100644 --- a/IceCubesAppIntents/TabIntent.swift +++ b/IceCubesAppIntents/TabIntent.swift @@ -35,7 +35,7 @@ enum TabEnum: String, AppEnum, Sendable { .post: .init(title: "New post")] } - var toAppTab: Tab { + var toAppTab: AppTab { switch self { case .timeline: .timeline diff --git a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift index 3fa869f9..6b3f3cd1 100644 --- a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift +++ b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift @@ -23,8 +23,11 @@ struct AccountWidgetProvider: AppIntentTimelineProvider { } private func fetchAccount(configuration: AccountWidgetConfiguration) async -> Account { - let client = Client(server: configuration.account.account.server, - oauthToken: configuration.account.account.oauthToken) + guard let account = configuration.account else { + return .placeholder() + } + let client = Client(server: account.account.server, + oauthToken: account.account.oauthToken) do { let account: Account = try await client.get(endpoint: Accounts.verifyCredentials) return account diff --git a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift index a21f5201..ffd48730 100644 --- a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift @@ -6,7 +6,7 @@ struct AccountWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account for this widget") @Parameter(title: "Account") - var account: AppAccountEntity + var account: AppAccountEntity? } extension AccountWidgetConfiguration { diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift index 82b020d6..42652ed8 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift @@ -29,9 +29,16 @@ struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline { do { - let timeline: TimelineFilter = .hashtag(tag: configuration.hashgtag, accountId: nil) + guard let account = configuration.account, let hashgtag = configuration.hashgtag else { + return Timeline(entries: [.init(date: Date(), + title: "#Mastodon", + statuses: [], + images: [:])], + policy: .atEnd) + } + let timeline: TimelineFilter = .hashtag(tag: hashgtag, accountId: nil) let statuses = await loadStatuses(for: timeline, - account: configuration.account, + account: account, widgetFamily: context.family) let images = try await loadImages(urls: statuses.map { $0.account.avatar }) return Timeline(entries: [.init(date: Date(), diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift index 74d767a9..df01d4d0 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift @@ -6,10 +6,10 @@ struct HashtagPostsWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account and hashtag for this widget") @Parameter(title: "Account") - var account: AppAccountEntity + var account: AppAccountEntity? @Parameter(title: "Hashtag") - var hashgtag: String + var hashgtag: String? } extension HashtagPostsWidgetConfiguration { diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift index a79111b5..b7df808e 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift @@ -18,7 +18,7 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { return entry } return .init(date: Date(), - title: configuration.timeline.timeline.title, + title: configuration.timeline?.timeline.title ?? "", statuses: [], images: [:]) } @@ -29,17 +29,24 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline { do { - let statuses = await loadStatuses(for: configuration.timeline.timeline, - account: configuration.account, + guard let timeline = configuration.timeline, let account = configuration.account else { + return Timeline(entries: [.init(date: Date(), + title: "", + statuses: [], + images: [:])], + policy: .atEnd) + } + let statuses = await loadStatuses(for: timeline.timeline, + account: account, widgetFamily: context.family) let images = try await loadImages(urls: statuses.map { $0.account.avatar }) return Timeline(entries: [.init(date: Date(), - title: configuration.timeline.timeline.title, + title: timeline.timeline.title, statuses: statuses, images: images)], policy: .atEnd) } catch { return Timeline(entries: [.init(date: Date(), - title: configuration.timeline.timeline.title, + title: configuration.timeline?.timeline.title ?? "", statuses: [], images: [:])], policy: .atEnd) diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift index e4dbed5b..4c30704b 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift @@ -6,10 +6,10 @@ struct LatestPostsWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account and timeline for this widget") @Parameter(title: "Account") - var account: AppAccountEntity + var account: AppAccountEntity? @Parameter(title: "Timeline") - var timeline: TimelineFilterEntity + var timeline: TimelineFilterEntity? } extension LatestPostsWidgetConfiguration { diff --git a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift index 1cc488b2..42cebd3f 100644 --- a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift +++ b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift @@ -29,13 +29,20 @@ struct ListsWidgetProvider: AppIntentTimelineProvider { private func timeline(for configuration: ListsWidgetConfiguration, context: Context) async -> Timeline { do { - let timeline: TimelineFilter = .list(list: configuration.timeline.list) - let statuses = await loadStatuses(for: timeline, - account: configuration.account, + guard let account = configuration.account, let timeline = configuration.timeline else { + return Timeline(entries: [.init(date: Date(), + title: "List name", + statuses: [], + images: [:])], + policy: .atEnd) + } + let filter: TimelineFilter = .list(list: timeline.list) + let statuses = await loadStatuses(for: filter, + account: account, widgetFamily: context.family) let images = try await loadImages(urls: statuses.map { $0.account.avatar }) return Timeline(entries: [.init(date: Date(), - title: timeline.title, + title: filter.title, statuses: statuses, images: images)], policy: .atEnd) } catch { diff --git a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift index 62015f6c..22f019aa 100644 --- a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift @@ -2,14 +2,14 @@ import AppIntents import WidgetKit struct ListsWidgetConfiguration: WidgetConfigurationIntent { - static let title: LocalizedStringResource = "Configuration" + static let title: LocalizedStringResource = "List Widget Configuration" static let description = IntentDescription("Choose the account and list for this widget") @Parameter(title: "Account") - var account: AppAccountEntity + var account: AppAccountEntity? @Parameter(title: "List") - var timeline: ListEntity + var timeline: ListEntity? } extension ListsWidgetConfiguration { diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift index a3d2616b..500a7bd8 100644 --- a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift @@ -29,8 +29,15 @@ struct MentionsWidgetProvider: AppIntentTimelineProvider { private func timeline(for configuration: MentionsWidgetConfiguration, context _: Context) async -> Timeline { do { - let client = Client(server: configuration.account.account.server, - oauthToken: configuration.account.account.oauthToken) + guard let account = configuration.account else { + return Timeline(entries: [.init(date: Date(), + title: "Mentions", + statuses: [], + images: [:])], + policy: .atEnd) + } + let client = Client(server: account.account.server, + oauthToken: account.account.oauthToken) var excludedTypes = Models.Notification.NotificationType.allCases excludedTypes.removeAll(where: { $0 == .mention }) let notifications: [Models.Notification] = diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift index 9dbe64a3..3e04faba 100644 --- a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift @@ -6,7 +6,7 @@ struct MentionsWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account for this widget") @Parameter(title: "Account") - var account: AppAccountEntity + var account: AppAccountEntity? } extension MentionsWidgetConfiguration { diff --git a/IceCubesNotifications/NotificationService.swift b/IceCubesNotifications/NotificationService.swift index 25e48996..c5abf884 100644 --- a/IceCubesNotifications/NotificationService.swift +++ b/IceCubesNotifications/NotificationService.swift @@ -1,7 +1,7 @@ import AppAccount import CryptoKit import Env -@preconcurrency import Intents +import Intents import KeychainSwift import Models import Network @@ -9,95 +9,101 @@ import Notifications import UIKit import UserNotifications +extension UNMutableNotificationContent: @unchecked @retroactive Sendable { } + class NotificationService: UNNotificationServiceExtension { - var contentHandler: ((UNNotificationContent) -> Void)? + override func didReceive(_ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + + let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent + let provider = NotificationServiceContentProvider(bestAttemptContent: bestAttemptContent) + let casted = unsafeBitCast(contentHandler, + to: (@Sendable (UNNotificationContent) -> Void).self) + Task { + if let content = await provider.buildContent() { + casted(content) + } + } + } +} + +actor NotificationServiceContentProvider { var bestAttemptContent: UNMutableNotificationContent? - - @MainActor override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - self.contentHandler = contentHandler - bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - + + private let pushKeys = PushKeys() + private let keychainAccounts = AppAccount.retrieveAll() + + init(bestAttemptContent: UNMutableNotificationContent? = nil) { + self.bestAttemptContent = bestAttemptContent + } + + func buildContent() async -> UNMutableNotificationContent? { if var bestAttemptContent { - let privateKey = PushNotificationsService.shared.notificationsPrivateKeyAsKey - let auth = PushNotificationsService.shared.notificationsAuthKeyAsKey - + let privateKey = pushKeys.notificationsPrivateKeyAsKey + let auth = pushKeys.notificationsAuthKeyAsKey + guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String, let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) else { - contentHandler(bestAttemptContent) - return + return bestAttemptContent } - + guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String, let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()), let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) else { - contentHandler(bestAttemptContent) - return + return bestAttemptContent } - + guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String, let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64()) else { - contentHandler(bestAttemptContent) - return + return bestAttemptContent } - + guard let plaintextData = NotificationService.decrypt(payload: payload, salt: salt, auth: auth, privateKey: privateKey, publicKey: publicKey), - let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) + let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) else { - contentHandler(bestAttemptContent) - return + return bestAttemptContent } - + bestAttemptContent.title = notification.title - if AppAccountsManager.shared.availableAccounts.count > 1 { + if keychainAccounts.count > 1 { bestAttemptContent.subtitle = bestAttemptContent.userInfo["i"] as? String ?? "" } bestAttemptContent.body = notification.body.escape() bestAttemptContent.userInfo["plaintext"] = plaintextData bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "glass.caf")) - - let preferences = UserPreferences.shared - let tokens = AppAccountsManager.shared.pushAccounts.map(\.token) - preferences.reloadNotificationsCount(tokens: tokens) - - if let token = AppAccountsManager.shared.availableAccounts.first(where: { $0.oauthToken?.accessToken == notification.accessToken })?.oauthToken { - var currentCount = preferences.notificationsCount[token] ?? 0 - currentCount += 1 - preferences.notificationsCount[token] = currentCount - } - - bestAttemptContent.badge = .init(integerLiteral: preferences.totalNotificationsCount) - + let badgeCount = await updateBadgeCoung(notification: notification) + bestAttemptContent.badge = .init(integerLiteral: badgeCount) + 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 { - // Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated - // context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor - // boundary. - // This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`. - // There is a Radar tracking this & others like it. - if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) { - if let image = UIImage(data: data) { - try? image.pngData()?.write(to: fileURL) - - if let remoteNotification = await toRemoteNotification(localNotification: notification), - let type = remoteNotification.supportedType - { - let intent = buildMessageIntent(remoteNotification: remoteNotification, - currentUser: bestAttemptContent.userInfo["i"] as? String ?? "", - avatarURL: fileURL) + + // Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated + // context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor + // boundary. + // This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`. + // There is a Radar tracking this & others like it. + if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) { + if let image = UIImage(data: data) { + try? image.pngData()?.write(to: fileURL) + + if let remoteNotification = await toRemoteNotification(localNotification: notification), + let type = remoteNotification.supportedType + { + let intent = buildMessageIntent(remoteNotification: remoteNotification, + currentUser: bestAttemptContent.userInfo["i"] as? String ?? "", + avatarURL: fileURL) + do { bestAttemptContent = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent bestAttemptContent.threadIdentifier = remoteNotification.type if type == .mention { @@ -106,27 +112,32 @@ class NotificationService: UNNotificationServiceExtension { let newBody = "\(NSLocalizedString(type.notificationKey(), bundle: .main, comment: ""))\(notification.body.escape())" bestAttemptContent.body = newBody } - } else { - if let attachment = try? UNNotificationAttachment(identifier: filename, url: fileURL, options: nil) { - bestAttemptContent.attachments = [attachment] - } + return bestAttemptContent + } catch { + return bestAttemptContent + } + } else { + if let attachment = try? UNNotificationAttachment(identifier: filename, + url: fileURL, + options: nil) { + bestAttemptContent.attachments = [attachment] } } - contentHandler(bestAttemptContent) - } else { - contentHandler(bestAttemptContent) } + } else { + return bestAttemptContent } } else { - contentHandler(bestAttemptContent) + return bestAttemptContent } } + return nil } - - @MainActor + + private func toRemoteNotification(localNotification: MastodonPushNotification) async -> Models.Notification? { do { - if let account = AppAccountsManager.shared.availableAccounts.first(where: { $0.oauthToken?.accessToken == localNotification.accessToken }) { + if let account = keychainAccounts.first(where: { $0.oauthToken?.accessToken == localNotification.accessToken }) { let client = Client(server: account.server, oauthToken: account.oauthToken) let remoteNotification: Models.Notification = try await client.get(endpoint: Notifications.notification(id: String(localNotification.notificationID))) return remoteNotification @@ -136,8 +147,7 @@ class NotificationService: UNNotificationServiceExtension { } return nil } - - @MainActor + private func buildMessageIntent(remoteNotification: Models.Notification, currentUser: String, avatarURL: URL) -> INSendMessageIntent @@ -152,7 +162,7 @@ class NotificationService: UNNotificationServiceExtension { customIdentifier: nil) var recipents: [INPerson]? var groupName: INSpeakableString? - if AppAccountsManager.shared.availableAccounts.count > 1 { + if keychainAccounts.count > 1 { let me = INPerson(personHandle: .init(value: currentUser, type: .unknown), nameComponents: nil, displayName: currentUser, @@ -175,4 +185,18 @@ class NotificationService: UNNotificationServiceExtension { } return intent } + + @MainActor + private func updateBadgeCoung(notification: MastodonPushNotification) -> Int { + let preferences = UserPreferences.shared + let tokens = AppAccountsManager.shared.pushAccounts.map(\.token) + preferences.reloadNotificationsCount(tokens: tokens) + + if let token = keychainAccounts.first(where: { $0.oauthToken?.accessToken == notification.accessToken })?.oauthToken { + var currentCount = preferences.notificationsCount[token] ?? 0 + currentCount += 1 + preferences.notificationsCount[token] = currentCount + } + return preferences.totalNotificationsCount + } } diff --git a/Packages/Account/Package.swift b/Packages/Account/Package.swift index c18c6703..eccfa71a 100644 --- a/Packages/Account/Package.swift +++ b/Packages/Account/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -36,7 +36,7 @@ let package = Package( .product(name: "WrappingHStack", package: "WrappingHStack"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), .testTarget( diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index ade97871..ee5ec31d 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -27,18 +27,14 @@ public struct AccountDetailView: View { @State private var displayTitle: Bool = false - @Binding var scrollToTopSignal: Int - /// When coming from a URL like a mention tap in a status. - public init(accountId: String, scrollToTopSignal: Binding) { + public init(accountId: String) { _viewModel = .init(initialValue: .init(accountId: accountId)) - _scrollToTopSignal = scrollToTopSignal } /// When the account is already fetched by the parent caller. - public init(account: Account, scrollToTopSignal: Binding) { + public init(account: Account) { _viewModel = .init(initialValue: .init(account: account)) - _scrollToTopSignal = scrollToTopSignal } public var body: some View { @@ -103,11 +99,6 @@ public struct AccountDetailView: View { .scrollContentBackground(.hidden) .background(theme.primaryBackgroundColor) #endif - .onChange(of: scrollToTopSignal) { - withAnimation { - proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top) - } - } } .onAppear { guard reasons != .placeholder else { return } @@ -416,6 +407,6 @@ extension View { struct AccountDetailView_Previews: PreviewProvider { static var previews: some View { - AccountDetailView(account: .placeholder(), scrollToTopSignal: .constant(0)) + AccountDetailView(account: .placeholder()) } } diff --git a/Packages/Account/Sources/Account/Filters/EditFilterView.swift b/Packages/Account/Sources/Account/Filters/EditFilterView.swift index 2539ff8c..c51e511b 100644 --- a/Packages/Account/Sources/Account/Filters/EditFilterView.swift +++ b/Packages/Account/Sources/Account/Filters/EditFilterView.swift @@ -118,7 +118,7 @@ struct EditFilterView: View { .focused($focusedField, equals: .title) .onSubmit { Task { - await saveFilter() + await saveFilter(client) } } } @@ -130,7 +130,7 @@ struct EditFilterView: View { Section { Button { Task { - await saveFilter() + await saveFilter(client) } } label: { if isSavingFilter { @@ -158,7 +158,7 @@ struct EditFilterView: View { Spacer() Button { Task { - await deleteKeyword(keyword: keyword) + await deleteKeyword(client, keyword: keyword) } } label: { Image(systemName: "trash") @@ -170,7 +170,7 @@ struct EditFilterView: View { if let index = indexes.first { let keyword = keywords[index] Task { - await deleteKeyword(keyword: keyword) + await deleteKeyword(client, keyword: keyword) } } } @@ -179,7 +179,7 @@ struct EditFilterView: View { .focused($focusedField, equals: .newKeyword) .onSubmit { Task { - await addKeyword(name: newKeyword) + await addKeyword(client, name: newKeyword) newKeyword = "" focusedField = .newKeyword } @@ -189,7 +189,7 @@ struct EditFilterView: View { Button { Task { Task { - await addKeyword(name: newKeyword) + await addKeyword(client, name: newKeyword) newKeyword = "" } } @@ -217,7 +217,7 @@ struct EditFilterView: View { contexts.append(context) } Task { - await saveFilter() + await saveFilter(client) } })) { Label(context.name, systemImage: context.iconName) @@ -242,7 +242,7 @@ struct EditFilterView: View { } .onChange(of: filterAction) { Task { - await saveFilter() + await saveFilter(client) } } .pickerStyle(.inline) @@ -256,11 +256,11 @@ struct EditFilterView: View { Button { Task { if !newKeyword.isEmpty { - await addKeyword(name: newKeyword) + await addKeyword(client, name: newKeyword) newKeyword = "" focusedField = .newKeyword } - await saveFilter() + await saveFilter(client) dismiss() } } label: { @@ -273,7 +273,7 @@ struct EditFilterView: View { .disabled(!canSave) } - private func saveFilter() async { + private func saveFilter(_ client: Client) async { do { isSavingFilter = true if let filter { @@ -288,7 +288,7 @@ struct EditFilterView: View { isSavingFilter = false } - private func addKeyword(name: String) async { + private func addKeyword(_ client: Client, name: String) async { guard let filterId = filter?.id else { return } isSavingFilter = true do { @@ -302,7 +302,7 @@ struct EditFilterView: View { isSavingFilter = false } - private func deleteKeyword(keyword: ServerFilter.Keyword) async { + private func deleteKeyword(_ client: Client, keyword: ServerFilter.Keyword) async { isSavingFilter = true do { let response = try await client.delete(endpoint: ServerFilters.removeKeyword(id: keyword.id), diff --git a/Packages/Account/Sources/Account/Filters/FiltersListView.swift b/Packages/Account/Sources/Account/Filters/FiltersListView.swift index 75628d38..33b1d296 100644 --- a/Packages/Account/Sources/Account/Filters/FiltersListView.swift +++ b/Packages/Account/Sources/Account/Filters/FiltersListView.swift @@ -50,7 +50,7 @@ public struct FiltersListView: View { } } .onDelete { indexes in - deleteFilter(indexes: indexes) + deleteFilter(client, indexes: indexes) } } } @@ -89,7 +89,7 @@ public struct FiltersListView: View { } } - private func deleteFilter(indexes: IndexSet) { + private func deleteFilter(_ client: Client, indexes: IndexSet) { if let index = indexes.first { Task { do { diff --git a/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift b/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift index 3fe520c6..443d3ef2 100644 --- a/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift +++ b/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift @@ -103,7 +103,6 @@ public struct AccountDetailMediaGridView: View { } private func fetchNextPage() async throws { - let client = client let newStatuses: [Status] = try await client.get(endpoint: Accounts.statuses(id: account.id, sinceId: mediaStatuses.last?.id, diff --git a/Packages/AppAccount/Package.swift b/Packages/AppAccount/Package.swift index 9bc861b6..26fba487 100644 --- a/Packages/AppAccount/Package.swift +++ b/Packages/AppAccount/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -32,7 +32,7 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), ] diff --git a/Packages/Conversations/Package.swift b/Packages/Conversations/Package.swift index dce06f6d..5c92622d 100644 --- a/Packages/Conversations/Package.swift +++ b/Packages/Conversations/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -32,7 +32,7 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), ] diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift index 2621c984..61ae9387 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift @@ -125,7 +125,8 @@ struct ConversationMessageView: View { Label(isLiked ? "status.action.unfavorite" : "status.action.favorite", systemImage: isLiked ? "star.fill" : "star") } - Button { Task { + Button { + Task { do { let status: Status if isBookmarked { diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift index bffc0460..4bd49c97 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift @@ -14,11 +14,7 @@ public struct ConversationsListView: View { @State private var viewModel = ConversationsListViewModel() - @Binding var scrollToTopSignal: Int - - public init(scrollToTopSignal: Binding) { - _scrollToTopSignal = scrollToTopSignal - } + public init() { } private var conversations: Binding<[Conversation]> { if viewModel.isLoadingFirstPage { @@ -89,11 +85,6 @@ public struct ConversationsListView: View { viewModel.handleEvent(event: latestEvent) } } - .onChange(of: scrollToTopSignal) { - withAnimation { - proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top) - } - } .refreshable { // note: this Task wrapper should not be necessary, but it reportedly crashes without it // when refreshing on an empty list diff --git a/Packages/DesignSystem/Package.swift b/Packages/DesignSystem/Package.swift index bb212390..fed10cea 100644 --- a/Packages/DesignSystem/Package.swift +++ b/Packages/DesignSystem/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -33,7 +33,7 @@ let package = Package( .product(name: "EmojiText", package: "EmojiText"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), ] diff --git a/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift b/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift index 84028a8c..409f5caa 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.swift @@ -18,7 +18,7 @@ public extension Color { } } -extension Color: RawRepresentable { +extension Color: @retroactive RawRepresentable { public init?(rawValue: Int) { let red = Double((rawValue & 0xFF0000) >> 16) / 0xFF let green = Double((rawValue & 0x00FF00) >> 8) / 0xFF diff --git a/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift b/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift index 297252e6..f67980e3 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift @@ -29,6 +29,13 @@ import UIKit override public init() { super.init() + + Task { @MainActor in + setup() + } + } + + private func setup() { #if os(visionOS) windowWidth = window?.bounds.size.width ?? 0 windowHeight = window?.bounds.size.height ?? 0 @@ -40,12 +47,6 @@ import UIKit _ = Self.observer // just for activating the lazy static property } - deinit { - Task { @MainActor in - Self.observedSceneDelegate.remove(self) - } - } - private static var observedSceneDelegate: Set = [] private static let observer = Task { @MainActor in while true { diff --git a/Packages/Env/Package.swift b/Packages/Env/Package.swift index c322a3d2..f02cdfe6 100644 --- a/Packages/Env/Package.swift +++ b/Packages/Env/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -32,7 +32,7 @@ let package = Package( .product(name: "TelemetryDeck", package: "SwiftSDK") ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), .testTarget( diff --git a/Packages/Env/Sources/Env/CustomEnvValues.swift b/Packages/Env/Sources/Env/CustomEnvValues.swift index 2911b5c8..5e6b88ac 100644 --- a/Packages/Env/Sources/Env/CustomEnvValues.swift +++ b/Packages/Env/Sources/Env/CustomEnvValues.swift @@ -1,94 +1,15 @@ import Foundation import SwiftUI -private struct SecondaryColumnKey: EnvironmentKey { - static let defaultValue = false -} - -private struct ExtraLeadingInset: EnvironmentKey { - static let defaultValue: CGFloat = 0 -} - -private struct IsCompact: EnvironmentKey { - static let defaultValue: Bool = false -} - -private struct IsMediaCompact: EnvironmentKey { - static let defaultValue: Bool = false -} - -private struct IsModal: EnvironmentKey { - static let defaultValue: Bool = false -} - -private struct IsInCaptureMode: EnvironmentKey { - static let defaultValue: Bool = false -} - -private struct IsSupporter: EnvironmentKey { - static let defaultValue: Bool = false -} - -private struct IsStatusFocused: EnvironmentKey { - static let defaultValue: Bool = false -} - -private struct IsHomeTimeline: EnvironmentKey { - static let defaultValue: Bool = false -} - -private struct IndentationLevel: EnvironmentKey { - static let defaultValue: UInt = 0 -} - -public extension EnvironmentValues { - var isSecondaryColumn: Bool { - get { self[SecondaryColumnKey.self] } - set { self[SecondaryColumnKey.self] = newValue } - } - - var extraLeadingInset: CGFloat { - get { self[ExtraLeadingInset.self] } - set { self[ExtraLeadingInset.self] = newValue } - } - - var isCompact: Bool { - get { self[IsCompact.self] } - set { self[IsCompact.self] = newValue } - } - - var isMediaCompact: Bool { - get { self[IsMediaCompact.self] } - set { self[IsMediaCompact.self] = newValue } - } - - var isModal: Bool { - get { self[IsModal.self] } - set { self[IsModal.self] = newValue } - } - - var isInCaptureMode: Bool { - get { self[IsInCaptureMode.self] } - set { self[IsInCaptureMode.self] = newValue } - } - - var isSupporter: Bool { - get { self[IsSupporter.self] } - set { self[IsSupporter.self] = newValue } - } - - var isStatusFocused: Bool { - get { self[IsStatusFocused.self] } - set { self[IsStatusFocused.self] = newValue } - } - - var indentationLevel: UInt { - get { self[IndentationLevel.self] } - set { self[IndentationLevel.self] = newValue } - } - - var isHomeTimeline: Bool { - get { self[IsHomeTimeline.self] } - set { self[IsHomeTimeline.self] = newValue } - } +extension EnvironmentValues { + @Entry public var isSecondaryColumn: Bool = false + @Entry public var extraLeadingInset: CGFloat = 0 + @Entry public var isCompact: Bool = false + @Entry public var isMediaCompact: Bool = false + @Entry public var isModal: Bool = false + @Entry public var isInCaptureMode: Bool = false + @Entry public var isSupporter: Bool = false + @Entry public var isStatusFocused: Bool = false + @Entry public var isHomeTimeline: Bool = false + @Entry public var indentationLevel: UInt = 0 } diff --git a/Packages/Env/Sources/Env/Ext/AppStorage.swift b/Packages/Env/Sources/Env/Ext/AppStorage.swift index 39a3eba3..2fa804a5 100644 --- a/Packages/Env/Sources/Env/Ext/AppStorage.swift +++ b/Packages/Env/Sources/Env/Ext/AppStorage.swift @@ -1,6 +1,6 @@ import Foundation -extension Array: RawRepresentable where Element: Codable { +extension Array: @retroactive RawRepresentable where Element: Codable { public init?(rawValue: String) { guard let data = rawValue.data(using: .utf8), let result = try? JSONDecoder().decode([Element].self, from: data) diff --git a/Packages/Env/Sources/Env/PushNotificationsService.swift b/Packages/Env/Sources/Env/PushNotificationsService.swift index f271a6dd..54ce9bf5 100644 --- a/Packages/Env/Sources/Env/PushNotificationsService.swift +++ b/Packages/Env/Sources/Env/PushNotificationsService.swift @@ -8,48 +8,18 @@ import Observation import SwiftUI import UserNotifications -extension UNNotificationResponse: @unchecked Sendable {} -extension UNUserNotificationCenter: @unchecked Sendable {} +extension UNNotification: @unchecked @retroactive Sendable {} +extension UNNotificationResponse: @unchecked @retroactive Sendable {} +extension UNUserNotificationCenter: @unchecked @retroactive Sendable {} -public struct PushAccount: Equatable { - public let server: String - public let token: OauthToken - public let accountName: String? - - public init(server: String, token: OauthToken, accountName: String?) { - self.server = server - self.token = token - self.accountName = accountName - } -} - -public struct HandledNotification: Equatable { - public let account: PushAccount - public let notification: Models.Notification -} - -@MainActor -@Observable public class PushNotificationsService: NSObject { +public struct PushKeys: Sendable { enum Constants { - static let endpoint = "https://icecubesrelay.fly.dev" static let keychainAuthKey = "notifications_auth_key" static let keychainPrivateKey = "notifications_private_key" } - - public static let shared = PushNotificationsService() - - public private(set) var subscriptions: [PushNotificationSubscriptionSettings] = [] - - public var pushToken: Data? - - public var handledNotification: HandledNotification? - - override init() { - super.init() - - UNUserNotificationCenter.current().delegate = self - } - + + public init() { } + private var keychain: KeychainSwift { let keychain = KeychainSwift() #if !DEBUG && !targetEnvironment(simulator) @@ -57,44 +27,7 @@ public struct HandledNotification: Equatable { #endif return keychain } - - public func requestPushNotifications() { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - } - - public func setAccounts(accounts: [PushAccount]) { - subscriptions = [] - for account in accounts { - let sub = PushNotificationSubscriptionSettings(account: account, - key: notificationsPrivateKeyAsKey.publicKey.x963Representation, - authKey: notificationsAuthKeyAsKey, - pushToken: pushToken) - subscriptions.append(sub) - } - } - - public func updateSubscriptions(forceCreate: Bool) async { - for subscription in subscriptions { - await withTaskGroup(of: Void.self, body: { group in - group.addTask { - await subscription.fetchSubscription() - if await subscription.subscription != nil, !forceCreate { - await subscription.deleteSubscription() - await subscription.updateSubscription() - } else if forceCreate { - await subscription.updateSubscription() - } - } - }) - } - } - - // MARK: - Key management - + public var notificationsPrivateKeyAsKey: P256.KeyAgreement.PrivateKey { if let key = keychain.get(Constants.keychainPrivateKey), let data = Data(base64Encoded: key) @@ -130,7 +63,7 @@ public struct HandledNotification: Equatable { return key } } - + private static func makeRandomNotificationsAuthKey() -> Data { let byteCount = 16 var bytes = Data(count: byteCount) @@ -139,6 +72,81 @@ public struct HandledNotification: Equatable { } } +public struct PushAccount: Equatable { + public let server: String + public let token: OauthToken + public let accountName: String? + + public init(server: String, token: OauthToken, accountName: String?) { + self.server = server + self.token = token + self.accountName = accountName + } +} + +public struct HandledNotification: Equatable { + public let account: PushAccount + public let notification: Models.Notification +} + +@MainActor +@Observable public class PushNotificationsService: NSObject { + enum Constants { + static let endpoint = "https://icecubesrelay.fly.dev" + } + + public static let shared = PushNotificationsService() + + private let pushKeys = PushKeys() + + public private(set) var subscriptions: [PushNotificationSubscriptionSettings] = [] + + public var pushToken: Data? + + public var handledNotification: HandledNotification? + + override init() { + super.init() + + UNUserNotificationCenter.current().delegate = self + } + + public func requestPushNotifications() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { @Sendable _, _ in + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } + } + } + + public func setAccounts(accounts: [PushAccount]) { + subscriptions = [] + for account in accounts { + let sub = PushNotificationSubscriptionSettings(account: account, + key: pushKeys.notificationsPrivateKeyAsKey.publicKey.x963Representation, + authKey: pushKeys.notificationsAuthKeyAsKey, + pushToken: pushToken) + subscriptions.append(sub) + } + } + + public func updateSubscriptions(forceCreate: Bool) async { + for subscription in subscriptions { + await withTaskGroup(of: Void.self, body: { group in + group.addTask { + await subscription.fetchSubscription() + if await subscription.subscription != nil, !forceCreate { + await subscription.deleteSubscription() + await subscription.updateSubscription() + } else if forceCreate { + await subscription.updateSubscription() + } + } + }) + } + } +} + extension PushNotificationsService: UNUserNotificationCenterDelegate { public func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { guard let plaintext = response.notification.request.content.userInfo["plaintext"] as? Data, diff --git a/Packages/Env/Sources/Env/StreamWatcher.swift b/Packages/Env/Sources/Env/StreamWatcher.swift index f8690bab..33c2bace 100644 --- a/Packages/Env/Sources/Env/StreamWatcher.swift +++ b/Packages/Env/Sources/Env/StreamWatcher.swift @@ -97,8 +97,8 @@ import OSLog } let rawEvent = try decoder.decode(RawStreamEvent.self, from: data) logger.info("Stream update: \(rawEvent.event)") - if let event = rawEventToEvent(rawEvent: rawEvent) { - Task { @MainActor in + Task { @MainActor in + if let event = self.rawEventToEvent(rawEvent: rawEvent) { self.events.append(event) self.latestEvent = event if let event = event as? StreamEventNotification, event.notification.status?.visibility != .direct { @@ -114,10 +114,13 @@ import OSLog break } - receiveMessage() + Task { @MainActor in + self.receiveMessage() + } case .failure: - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(retryDelay)) { + Task { @MainActor in + try? await Task.sleep(for: .seconds(self.retryDelay)) self.retryDelay += 30 self.stopWatching() self.connect() diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index 9233e26a..2a1ae9f2 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -536,7 +536,7 @@ import SwiftUI } } -extension UInt: RawRepresentable { +extension UInt: @retroactive RawRepresentable { public var rawValue: Int { Int(self) } diff --git a/Packages/Env/Tests/RouterTests.swift b/Packages/Env/Tests/RouterTests.swift index d14fd627..d1522beb 100644 --- a/Packages/Env/Tests/RouterTests.swift +++ b/Packages/Env/Tests/RouterTests.swift @@ -2,49 +2,57 @@ import Network import SwiftUI import XCTest +import Testing +@Test @MainActor -final class RouterTests: XCTestCase { - func testRouterThreadsURL() { - let router = RouterPath() - let url = URL(string: "https://www.threads.net/@dimillian")! - _ = router.handle(url: url) - XCTAssertTrue(router.path.isEmpty) - } - - func testRouterTagsURL() { - let router = RouterPath() - let url = URL(string: "https://mastodon.social/tags/test")! - _ = router.handle(url: url) - XCTAssertTrue(router.path.first == .hashTag(tag: "test", account: nil)) - } - - func testRouterLocalStatusURL() { - let router = RouterPath() - let client = Client(server: "mastodon.social", - oauthToken: .init(accessToken: "", tokenType: "", scope: "", createdAt: 0)) - client.addConnections(["mastodon.social"]) - router.client = client - let url = URL(string: "https://mastodon.social/status/1010384")! - _ = router.handle(url: url) - XCTAssertTrue(router.path.first == .statusDetail(id: "1010384")) - } - - func testRouterRemoteStatusURL() { - let router = RouterPath() - let client = Client(server: "mastodon.social", - oauthToken: .init(accessToken: "", tokenType: "", scope: "", createdAt: 0)) - client.addConnections(["mastodon.social", "mastodon.online"]) - router.client = client - let url = URL(string: "https://mastodon.online/status/1010384")! - _ = router.handle(url: url) - XCTAssertTrue(router.path.first == .remoteStatusDetail(url: url)) - } - - func testRouteRandomURL() { - let router = RouterPath() - let url = URL(string: "https://theweb.com/test/test/one")! - _ = router.handle(url: url) - XCTAssertTrue(router.path.isEmpty) - } +func testRouterThreadsURL() { + let router = RouterPath() + let url = URL(string: "https://www.threads.net/@dimillian")! + _ = router.handle(url: url) + #expect(router.path.isEmpty) +} + +@Test +@MainActor +func testRouterTagsURL() { + let router = RouterPath() + let url = URL(string: "https://mastodon.social/tags/test")! + _ = router.handle(url: url) + #expect(router.path.first == .hashTag(tag: "test", account: nil)) +} + +@Test +@MainActor +func testRouterLocalStatusURL() { + let router = RouterPath() + let client = Client(server: "mastodon.social", + oauthToken: .init(accessToken: "", tokenType: "", scope: "", createdAt: 0)) + client.addConnections(["mastodon.social"]) + router.client = client + let url = URL(string: "https://mastodon.social/status/1010384")! + _ = router.handle(url: url) + #expect(router.path.first == .statusDetail(id: "1010384")) +} + +@Test +@MainActor +func testRouterRemoteStatusURL() { + let router = RouterPath() + let client = Client(server: "mastodon.social", + oauthToken: .init(accessToken: "", tokenType: "", scope: "", createdAt: 0)) + client.addConnections(["mastodon.social", "mastodon.online"]) + router.client = client + let url = URL(string: "https://mastodon.online/status/1010384")! + _ = router.handle(url: url) + #expect(router.path.first == .remoteStatusDetail(url: url)) +} + +@Test +@MainActor +func testRouteRandomURL() { + let router = RouterPath() + let url = URL(string: "https://theweb.com/test/test/one")! + _ = router.handle(url: url) + #expect(router.path.isEmpty) } diff --git a/Packages/Explore/Package.swift b/Packages/Explore/Package.swift index c8b24a08..da8372ba 100644 --- a/Packages/Explore/Package.swift +++ b/Packages/Explore/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -36,7 +36,7 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), ] diff --git a/Packages/Explore/Sources/Explore/ExploreView.swift b/Packages/Explore/Sources/Explore/ExploreView.swift index 7209118a..792a92c8 100644 --- a/Packages/Explore/Sources/Explore/ExploreView.swift +++ b/Packages/Explore/Sources/Explore/ExploreView.swift @@ -14,11 +14,7 @@ public struct ExploreView: View { @State private var viewModel = ExploreViewModel() - @Binding var scrollToTopSignal: Int - - public init(scrollToTopSignal: Binding) { - _scrollToTopSignal = scrollToTopSignal - } + public init() { } public var body: some View { ScrollViewReader { proxy in @@ -111,15 +107,6 @@ public struct ExploreView: View { .task(id: viewModel.searchQuery) { await viewModel.search() } - .onChange(of: scrollToTopSignal) { - if viewModel.scrollToTopVisible { - viewModel.isSearchPresented.toggle() - } else { - withAnimation { - proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top) - } - } - } } } diff --git a/Packages/Lists/Package.swift b/Packages/Lists/Package.swift index 403f7920..37d3c2a4 100644 --- a/Packages/Lists/Package.swift +++ b/Packages/Lists/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -34,7 +34,7 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), ] diff --git a/Packages/Lists/Sources/Lists/Create/ListCreateView.swift b/Packages/Lists/Sources/Lists/Create/ListCreateView.swift index 344263da..ad2eed09 100644 --- a/Packages/Lists/Sources/Lists/Create/ListCreateView.swift +++ b/Packages/Lists/Sources/Lists/Create/ListCreateView.swift @@ -42,6 +42,7 @@ public struct ListCreateView: View { CancelToolbarItem() ToolbarItem { Button { + let client = client Task { isSaving = true let _: Models.List = try await client.post(endpoint: Lists.createList(title: title, diff --git a/Packages/MediaUI/Package.swift b/Packages/MediaUI/Package.swift index ba6c1e04..12745550 100644 --- a/Packages/MediaUI/Package.swift +++ b/Packages/MediaUI/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -28,7 +28,7 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), ] diff --git a/Packages/Models/Package.swift b/Packages/Models/Package.swift index d8edd7e4..0a95e5da 100644 --- a/Packages/Models/Package.swift +++ b/Packages/Models/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -26,7 +26,7 @@ let package = Package( "SwiftSoup", ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), .testTarget( diff --git a/Packages/Models/Sources/Models/Stream/StreamEvent.swift b/Packages/Models/Sources/Models/Stream/StreamEvent.swift index 1ba79d7d..c2a4dd6e 100644 --- a/Packages/Models/Sources/Models/Stream/StreamEvent.swift +++ b/Packages/Models/Sources/Models/Stream/StreamEvent.swift @@ -1,17 +1,17 @@ import Foundation -public struct RawStreamEvent: Decodable { +public struct RawStreamEvent: Decodable, Sendable{ public let event: String public let stream: [String] public let payload: String } -public protocol StreamEvent: Identifiable { +public protocol StreamEvent: Identifiable, Sendable { var date: Date { get } var id: String { get } } -public struct StreamEventUpdate: StreamEvent { +public struct StreamEventUpdate: StreamEvent, Sendable { public let date = Date() public var id: String { status.id } public let status: Status @@ -20,7 +20,7 @@ public struct StreamEventUpdate: StreamEvent { } } -public struct StreamEventStatusUpdate: StreamEvent { +public struct StreamEventStatusUpdate: StreamEvent, Sendable { public let date = Date() public var id: String { status.id + (status.editedAt?.asDate.description ?? "") } public let status: Status @@ -29,7 +29,7 @@ public struct StreamEventStatusUpdate: StreamEvent { } } -public struct StreamEventDelete: StreamEvent { +public struct StreamEventDelete: StreamEvent, Sendable { public let date = Date() public var id: String { status + date.description } public let status: String @@ -38,7 +38,7 @@ public struct StreamEventDelete: StreamEvent { } } -public struct StreamEventNotification: StreamEvent { +public struct StreamEventNotification: StreamEvent, Sendable { public let date = Date() public var id: String { notification.id } public let notification: Notification @@ -47,7 +47,7 @@ public struct StreamEventNotification: StreamEvent { } } -public struct StreamEventConversation: StreamEvent { +public struct StreamEventConversation: StreamEvent, Sendable { public let date = Date() public var id: String { conversation.id } public let conversation: Conversation diff --git a/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift b/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift index 1f674cd3..1bf3ab5b 100644 --- a/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift +++ b/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift @@ -1,88 +1,88 @@ @testable import Models -import XCTest +import Testing +import Foundation -final class HTMLStringTests: XCTestCase { - func testURLInit() throws { - XCTAssertNil(URL(string: "", encodePath: true)) +@Test +func testURLInit() throws { + let simpleUrl = URL(string: "https://www.google.com", encodePath: true) + #expect("https://www.google.com" == simpleUrl?.absoluteString) - let simpleUrl = URL(string: "https://www.google.com", encodePath: true) - XCTAssertEqual("https://www.google.com", simpleUrl?.absoluteString) + let urlWithTrailingSlash = URL(string: "https://www.google.com/", encodePath: true) + #expect("https://www.google.com/" == urlWithTrailingSlash?.absoluteString) - let urlWithTrailingSlash = URL(string: "https://www.google.com/", encodePath: true) - XCTAssertEqual("https://www.google.com/", urlWithTrailingSlash?.absoluteString) + let extendedCharPath = URL(string: "https://en.wikipedia.org/wiki/Elbbrücken_station", encodePath: true) + #expect("https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station" == extendedCharPath?.absoluteString) - let extendedCharPath = URL(string: "https://en.wikipedia.org/wiki/Elbbrücken_station", encodePath: true) - XCTAssertEqual("https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", extendedCharPath?.absoluteString) + let extendedCharQuery = URL(string: "http://test.com/blah/city?name=京都市", encodePath: true) + #expect("http://test.com/blah/city?name=%E4%BA%AC%E9%83%BD%E5%B8%82" == extendedCharQuery?.absoluteString) - let extendedCharQuery = URL(string: "http://test.com/blah/city?name=京都市", encodePath: true) - XCTAssertEqual("http://test.com/blah/city?name=%E4%BA%AC%E9%83%BD%E5%B8%82", extendedCharQuery?.absoluteString) - - // Double encoding will happen if you ask to encodePath on an already encoded string - let alreadyEncodedPath = URL(string: "https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", encodePath: true) - XCTAssertEqual("https://en.wikipedia.org/wiki/Elbbr%25C3%25BCcken_station", alreadyEncodedPath?.absoluteString) - } - - func testHTMLStringInit() throws { - let decoder = JSONDecoder() - - let basicContent = "\"

This is a test

\"" - var htmlString = try decoder.decode(HTMLString.self, from: Data(basicContent.utf8)) - XCTAssertEqual("This is a test", htmlString.asRawText) - XCTAssertEqual("

This is a test

", htmlString.htmlValue) - XCTAssertEqual("This is a test", htmlString.asMarkdown) - XCTAssertEqual(0, htmlString.statusesURLs.count) - XCTAssertEqual(0, htmlString.links.count) - - let basicLink = "\"

This is a test

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

This is a test

", htmlString.htmlValue) - XCTAssertEqual("This is a [test](https://test.com)", htmlString.asMarkdown) - XCTAssertEqual(0, htmlString.statusesURLs.count) - XCTAssertEqual(1, htmlString.links.count) - XCTAssertEqual("https://test.com", htmlString.links[0].url.absoluteString) - XCTAssertEqual("test", htmlString.links[0].displayString) - - let extendedCharLink = "\"

This is a test

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

This is a test

", htmlString.htmlValue) - XCTAssertEqual("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)", htmlString.asMarkdown) - XCTAssertEqual(0, htmlString.statusesURLs.count) - XCTAssertEqual(1, htmlString.links.count) - XCTAssertEqual("https://test.com/go%C3%9F%C3%AB%C3%B1a", htmlString.links[0].url.absoluteString) - XCTAssertEqual("test", htmlString.links[0].displayString) - - let alreadyEncodedLink = "\"

This is a test

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

This is a test

", htmlString.htmlValue) - XCTAssertEqual("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)", htmlString.asMarkdown) - XCTAssertEqual(0, htmlString.statusesURLs.count) - XCTAssertEqual(1, htmlString.links.count) - XCTAssertEqual("https://test.com/go%C3%9F%C3%AB%C3%B1a", htmlString.links[0].url.absoluteString) - XCTAssertEqual("test", htmlString.links[0].displayString) - } - - func testHTMLStringInit_markdownEscaping() throws { - let decoder = JSONDecoder() - - let stdMarkdownContent = "\"

This [*is*] `a`\\n**test**

\"" - var htmlString = try decoder.decode(HTMLString.self, from: Data(stdMarkdownContent.utf8)) - XCTAssertEqual("This [*is*] `a`\n**test**", htmlString.asRawText) - XCTAssertEqual("

This [*is*] `a`\n**test**

", htmlString.htmlValue) - XCTAssertEqual("This \\[\\*is\\*] \\`a\\` \\*\\*test\\*\\*", htmlString.asMarkdown) - - let underscoreContent = "\"

This _is_ an :emoji_maybe:

\"" - htmlString = try decoder.decode(HTMLString.self, from: Data(underscoreContent.utf8)) - XCTAssertEqual("This _is_ an :emoji_maybe:", htmlString.asRawText) - XCTAssertEqual("

This _is_ an :emoji_maybe:

", htmlString.htmlValue) - XCTAssertEqual("This \\_is\\_ an :emoji_maybe:", htmlString.asMarkdown) - - let strikeContent = "\"

This ~is~ a\\n`test`

\"" - htmlString = try decoder.decode(HTMLString.self, from: Data(strikeContent.utf8)) - XCTAssertEqual("This ~is~ a\n`test`", htmlString.asRawText) - XCTAssertEqual("

This ~is~ a\n`test`

", htmlString.htmlValue) - XCTAssertEqual("This \\~is\\~ a \\`test\\`", htmlString.asMarkdown) - } + // Double encoding will happen if you ask to encodePath on an already encoded string + let alreadyEncodedPath = URL(string: "https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", encodePath: true) + #expect("https://en.wikipedia.org/wiki/Elbbr%25C3%25BCcken_station" == alreadyEncodedPath?.absoluteString) +} + +@Test +func testHTMLStringInit() throws { + let decoder = JSONDecoder() + + let basicContent = "\"

This is a test

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

This is a test

" == htmlString.htmlValue) + #expect("This is a test" == htmlString.asMarkdown) + #expect(0 == htmlString.statusesURLs.count) + #expect(0 == htmlString.links.count) + + let basicLink = "\"

This is a test

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

This is a test

" == htmlString.htmlValue) + #expect("This is a [test](https://test.com)" == htmlString.asMarkdown) + #expect(0 == htmlString.statusesURLs.count) + #expect(1 == htmlString.links.count) + #expect("https://test.com" == htmlString.links[0].url.absoluteString) + #expect("test" == htmlString.links[0].displayString) + + let extendedCharLink = "\"

This is a test

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

This is a test

" == htmlString.htmlValue) + #expect("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)" == htmlString.asMarkdown) + #expect(0 == htmlString.statusesURLs.count) + #expect(1 == htmlString.links.count) + #expect("https://test.com/go%C3%9F%C3%AB%C3%B1a" == htmlString.links[0].url.absoluteString) + #expect("test" == htmlString.links[0].displayString) + + let alreadyEncodedLink = "\"

This is a test

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

This is a test

" == htmlString.htmlValue) + #expect("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)" == htmlString.asMarkdown) + #expect(0 == htmlString.statusesURLs.count) + #expect(1 == htmlString.links.count) + #expect("https://test.com/go%C3%9F%C3%AB%C3%B1a" == htmlString.links[0].url.absoluteString) + #expect("test" == htmlString.links[0].displayString) +} + +@Test +func testHTMLStringInit_markdownEscaping() throws { + let decoder = JSONDecoder() + + let stdMarkdownContent = "\"

This [*is*] `a`\\n**test**

\"" + var htmlString = try decoder.decode(HTMLString.self, from: Data(stdMarkdownContent.utf8)) + #expect("This [*is*] `a`\n**test**" == htmlString.asRawText) + #expect("

This [*is*] `a`\n**test**

" == htmlString.htmlValue) + #expect("This \\[\\*is\\*] \\`a\\` \\*\\*test\\*\\*" == htmlString.asMarkdown) + + let underscoreContent = "\"

This _is_ an :emoji_maybe:

\"" + htmlString = try decoder.decode(HTMLString.self, from: Data(underscoreContent.utf8)) + #expect("This _is_ an :emoji_maybe:" == htmlString.asRawText) + #expect("

This _is_ an :emoji_maybe:

" == htmlString.htmlValue) + #expect("This \\_is\\_ an :emoji_maybe:" == htmlString.asMarkdown) + + let strikeContent = "\"

This ~is~ a\\n`test`

\"" + htmlString = try decoder.decode(HTMLString.self, from: Data(strikeContent.utf8)) + #expect("This ~is~ a\n`test`" == htmlString.asRawText) + #expect("

This ~is~ a\n`test`

" == htmlString.htmlValue) + #expect("This \\~is\\~ a \\`test\\`" == htmlString.asMarkdown) } diff --git a/Packages/Network/Package.swift b/Packages/Network/Package.swift index 0c8e01fe..b9ccc459 100644 --- a/Packages/Network/Package.swift +++ b/Packages/Network/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -26,7 +26,7 @@ let package = Package( .product(name: "Models", package: "Models"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), .testTarget( diff --git a/Packages/Notifications/Package.swift b/Packages/Notifications/Package.swift index 149a2433..2727b932 100644 --- a/Packages/Notifications/Package.swift +++ b/Packages/Notifications/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -34,7 +34,7 @@ let package = Package( .product(name: "DesignSystem", package: "DesignSystem"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), ] diff --git a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift index a90f82f2..65d2f3a9 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift @@ -17,39 +17,29 @@ public struct NotificationsListView: View { @State private var viewModel = NotificationsViewModel() @State private var isNotificationsPolicyPresented: Bool = false - @Binding var scrollToTopSignal: Int let lockedType: Models.Notification.NotificationType? let lockedAccountId: String? public init(lockedType: Models.Notification.NotificationType? = nil, - lockedAccountId: String? = nil, - scrollToTopSignal: Binding) + lockedAccountId: String? = nil) { self.lockedType = lockedType self.lockedAccountId = lockedAccountId - _scrollToTopSignal = scrollToTopSignal } public var body: some View { - ScrollViewReader { proxy in - List { - scrollToTopView - topPaddingView - if lockedAccountId == nil, let summary = viewModel.policy?.summary { - NotificationsHeaderFilteredView(filteredNotifications: summary) - } - notificationsView - } - .id(account.account?.id) - .environment(\.defaultMinListRowHeight, 1) - .listStyle(.plain) - .onChange(of: scrollToTopSignal) { - withAnimation { - proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top) - } + List { + scrollToTopView + topPaddingView + if lockedAccountId == nil, let summary = viewModel.policy?.summary { + NotificationsHeaderFilteredView(filteredNotifications: summary) } + notificationsView } + .id(account.account?.id) + .environment(\.defaultMinListRowHeight, 1) + .listStyle(.plain) .toolbar { ToolbarItem(placement: .principal) { let title = lockedType?.menuTitle() ?? viewModel.selectedType?.menuTitle() ?? "notifications.navigation-title" @@ -75,7 +65,7 @@ public struct NotificationsListView: View { Button { viewModel.selectedType = nil Task { - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) } } label: { Label("notifications.navigation-title", systemImage: "bell.fill") @@ -85,7 +75,7 @@ public struct NotificationsListView: View { Button { viewModel.selectedType = type Task { - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) } } label: { Label { @@ -126,27 +116,27 @@ public struct NotificationsListView: View { viewModel.loadSelectedType() } Task { - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) await viewModel.fetchPolicy() } } .refreshable { SoundEffectManager.shared.playSound(.pull) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3)) - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7)) SoundEffectManager.shared.playSound(.refresh) } .onChange(of: watcher.latestEvent?.id) { if let latestEvent = watcher.latestEvent { - viewModel.handleEvent(event: latestEvent) + viewModel.handleEvent(selectedType: viewModel.selectedType, event: latestEvent) } } .onChange(of: scenePhase) { _, newValue in switch newValue { case .active: Task { - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) } default: break @@ -212,7 +202,7 @@ public struct NotificationsListView: View { EmptyView() case .hasNextPage: NextPageView { - try await viewModel.fetchNextPage() + try await viewModel.fetchNextPage(viewModel.selectedType) } .listRowInsets(.init(top: .layoutPadding, leading: .layoutPadding + 4, @@ -229,7 +219,7 @@ public struct NotificationsListView: View { message: "notifications.error.message", buttonTitle: "action.retry") { - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) } #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) diff --git a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift index 5197e413..33eff9d7 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift @@ -78,7 +78,7 @@ import SwiftUI private var consolidatedNotifications: [ConsolidatedNotification] = [] - func fetchNotifications() async { + func fetchNotifications(_ selectedType: Models.Notification.NotificationType?) async { guard let client, let currentAccount else { return } do { var nextPageState: State.PagingState = .hasNextPage @@ -150,7 +150,7 @@ import SwiftUI return allNotifications } - func fetchNextPage() async throws { + func fetchNextPage(_ selectedType: Models.Notification.NotificationType?) async throws { guard let client else { return } guard let lastId = consolidatedNotifications.last?.notificationIds.last else { return } let newNotifications: [Models.Notification] @@ -185,7 +185,7 @@ import SwiftUI policy = try? await client?.get(endpoint: Notifications.policy) } - func handleEvent(event: any StreamEvent) { + func handleEvent(selectedType: Models.Notification.NotificationType?, event: any StreamEvent) { Task { // Check if the event is a notification, // if it is not already in the list, diff --git a/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift b/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift index 65ddb46c..3ef60f8a 100644 --- a/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift +++ b/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift @@ -32,7 +32,7 @@ public struct NotificationsRequestsListView: View { message: "notifications.error.message", buttonTitle: "action.retry") { - await fetchRequests() + await fetchRequests(client) } #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) @@ -43,13 +43,13 @@ public struct NotificationsRequestsListView: View { NotificationsRequestsRowView(request: request) .swipeActions { Button { - Task { await acceptRequest(request) } + Task { await acceptRequest(client, request) } } label: { Label("account.follow-request.accept", systemImage: "checkmark") } Button { - Task { await dismissRequest(request) } + Task { await dismissRequest(client, request) } } label: { Label("account.follow-request.reject", systemImage: "xmark") } @@ -66,14 +66,14 @@ public struct NotificationsRequestsListView: View { .navigationTitle("notifications.content-filter.requests.title") .navigationBarTitleDisplayMode(.inline) .task { - await fetchRequests() + await fetchRequests(client) } .refreshable { - await fetchRequests() + await fetchRequests(client) } } - private func fetchRequests() async { + private func fetchRequests(_ client: Client) async { do { viewState = try .requests(await client.get(endpoint: Notifications.requests)) } catch { @@ -81,13 +81,13 @@ public struct NotificationsRequestsListView: View { } } - private func acceptRequest(_ request: NotificationsRequest) async { + private func acceptRequest(_ client: Client, _ request: NotificationsRequest) async { _ = try? await client.post(endpoint: Notifications.acceptRequest(id: request.id)) - await fetchRequests() + await fetchRequests(client) } - private func dismissRequest(_ request: NotificationsRequest) async { + private func dismissRequest(_ client: Client, _ request: NotificationsRequest) async { _ = try? await client.post(endpoint: Notifications.dismissRequest(id: request.id)) - await fetchRequests() + await fetchRequests(client) } } diff --git a/Packages/StatusKit/Package.swift b/Packages/StatusKit/Package.swift index f20bdab3..3f2575e5 100644 --- a/Packages/StatusKit/Package.swift +++ b/Packages/StatusKit/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -38,7 +38,7 @@ let package = Package( .product(name: "LRUCache", package: "LRUCache"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), ] diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/GIF/GIFPickerView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/GIF/GIFPickerView.swift index 054b0742..7c8467ed 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/GIF/GIFPickerView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/GIF/GIFPickerView.swift @@ -1,9 +1,10 @@ #if !os(visionOS) && !DEBUG import DesignSystem - import GiphyUISDK + @preconcurrency import GiphyUISDK import SwiftUI import UIKit - + + @MainActor struct GifPickerView: UIViewControllerRepresentable { @Environment(Theme.self) private var theme @@ -33,6 +34,7 @@ GifPickerView.Coordinator(parent: self) } + @MainActor class Coordinator: NSObject, GiphyDelegate { var parent: GifPickerView @@ -40,13 +42,17 @@ self.parent = parent } - func didDismiss(controller _: GiphyViewController?) { - parent.onShouldDismissGifPicker() + nonisolated func didDismiss(controller _: GiphyViewController?) { + Task { @MainActor in + parent.onShouldDismissGifPicker() + } } - func didSelectMedia(giphyViewController _: GiphyViewController, media: GPHMedia) { - let url = media.url(rendition: .fixedWidth, fileType: .gif) - parent.completion(url ?? "") + nonisolated func didSelectMedia(giphyViewController _: GiphyViewController, media: GPHMedia) { + Task { @MainActor in + let url = media.url(rendition: .fixedWidth, fileType: .gif) + parent.completion(url ?? "") + } } } } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/UTTypeSupported.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/UTTypeSupported.swift index fc158a3c..d8691545 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/UTTypeSupported.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/UTTypeSupported.swift @@ -1,5 +1,5 @@ -@preconcurrency import AVFoundation -import Foundation +import AVFoundation +@preconcurrency import Foundation import PhotosUI import SwiftUI import UIKit diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift index 5f570f39..39c8f26a 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift @@ -17,7 +17,7 @@ public extension StatusEditor { var client: Client? var currentAccount: Account? { didSet { - if let itemsProvider { + if itemsProvider != nil { mediaContainers = [] } } @@ -981,4 +981,4 @@ extension StatusEditor.ViewModel: UITextPasteDelegate { } } -extension PhotosPickerItem: @unchecked Sendable {} +extension PhotosPickerItem: @unchecked @retroactive Sendable {} diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift index 7cc4d2f8..92dab792 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift @@ -45,7 +45,8 @@ struct StatusActionButtonStyle: ButtonStyle { } } - struct SparklesView: View, Animatable { + @MainActor + struct SparklesView: View, @preconcurrency Animatable { var counter: Float var tint: Color var size: CGFloat diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift index 222f1769..04da7a45 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift @@ -66,7 +66,7 @@ struct StatusRowTranslateView: View { generalTranslateButton .onChange(of: preferences.preferredTranslationType) { _, _ in withAnimation { - _ = viewModel.updatePreferredTranslation() + viewModel.updatePreferredTranslation() } } diff --git a/Packages/Timeline/Package.swift b/Packages/Timeline/Package.swift index b0c48db0..258f5203 100644 --- a/Packages/Timeline/Package.swift +++ b/Packages/Timeline/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -38,7 +38,7 @@ let package = Package( .product(name: "Bodega", package: "Bodega"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), + .swiftLanguageMode(.v6), ] ), .testTarget( diff --git a/Packages/Timeline/Sources/Timeline/View/TimelineView.swift b/Packages/Timeline/Sources/Timeline/View/TimelineView.swift index 47db3c4d..e759d660 100644 --- a/Packages/Timeline/Sources/Timeline/View/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/View/TimelineView.swift @@ -27,7 +27,6 @@ public struct TimelineView: View { @Binding var timeline: TimelineFilter @Binding var pinnedFilters: [TimelineFilter] @Binding var selectedTagGroup: TagGroup? - @Binding var scrollToTopSignal: Int @Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup] @@ -36,82 +35,73 @@ public struct TimelineView: View { public init(timeline: Binding, pinnedFilters: Binding<[TimelineFilter]>, selectedTagGroup: Binding, - scrollToTopSignal: Binding, canFilterTimeline: Bool) { _timeline = timeline _pinnedFilters = pinnedFilters _selectedTagGroup = selectedTagGroup - _scrollToTopSignal = scrollToTopSignal self.canFilterTimeline = canFilterTimeline } public var body: some View { - ScrollViewReader { proxy in - ZStack(alignment: .top) { - List { - scrollToTopView - TimelineTagGroupheaderView(group: $selectedTagGroup, timeline: $timeline) - TimelineTagHeaderView(tag: $viewModel.tag) - switch viewModel.timeline { - case .remoteLocal: - StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath, isRemote: true) - default: - StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath) - .environment(\.isHomeTimeline, timeline == .home) + ZStack(alignment: .top) { + List { + scrollToTopView + TimelineTagGroupheaderView(group: $selectedTagGroup, timeline: $timeline) + TimelineTagHeaderView(tag: $viewModel.tag) + switch viewModel.timeline { + case .remoteLocal: + StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath, isRemote: true) + default: + StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath) + .environment(\.isHomeTimeline, timeline == .home) + } + } + .id(client.id) + .environment(\.defaultMinListRowHeight, 1) + .listStyle(.plain) + #if !os(visionOS) + .scrollContentBackground(.hidden) + .background(theme.primaryBackgroundColor) + #endif + .introspect(.list, on: .iOS(.v17, .v18)) { (collectionView: UICollectionView) in + DispatchQueue.main.async { + self.collectionView = collectionView } + prefetcher.viewModel = viewModel + collectionView.isPrefetchingEnabled = true + collectionView.prefetchDataSource = prefetcher } - .id(client.id) - .environment(\.defaultMinListRowHeight, 1) - .listStyle(.plain) - #if !os(visionOS) - .scrollContentBackground(.hidden) - .background(theme.primaryBackgroundColor) - #endif - .introspect(.list, on: .iOS(.v17, .v18)) { (collectionView: UICollectionView) in - DispatchQueue.main.async { - self.collectionView = collectionView - } - prefetcher.viewModel = viewModel - collectionView.isPrefetchingEnabled = true - collectionView.prefetchDataSource = prefetcher - } - if viewModel.timeline.supportNewestPagination { - TimelineUnreadStatusesView(observer: viewModel.pendingStatusesObserver) + if viewModel.timeline.supportNewestPagination { + TimelineUnreadStatusesView(observer: viewModel.pendingStatusesObserver) + } + } + .safeAreaInset(edge: .top, spacing: 0) { + if canFilterTimeline, !pinnedFilters.isEmpty { + VStack(spacing: 0) { + TimelineQuickAccessPills(pinnedFilters: $pinnedFilters, timeline: $timeline) + .padding(.vertical, 8) + .padding(.horizontal, .layoutPadding) + .background(theme.primaryBackgroundColor.opacity(0.30)) + .background(Material.ultraThin) + Divider() } } - .safeAreaInset(edge: .top, spacing: 0) { - if canFilterTimeline, !pinnedFilters.isEmpty { - VStack(spacing: 0) { - TimelineQuickAccessPills(pinnedFilters: $pinnedFilters, timeline: $timeline) - .padding(.vertical, 8) - .padding(.horizontal, .layoutPadding) - .background(theme.primaryBackgroundColor.opacity(0.30)) - .background(Material.ultraThin) - Divider() - } - } - } - .if(canFilterTimeline && !pinnedFilters.isEmpty) { view in - view.toolbarBackground(.hidden, for: .navigationBar) - } - .onChange(of: viewModel.scrollToIndex) { _, newValue in - if let collectionView, - let newValue, - let rows = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0), - rows > newValue - { - collectionView.scrollToItem(at: .init(row: newValue, section: 0), - at: .top, - animated: viewModel.scrollToIndexAnimated) - viewModel.scrollToIndexAnimated = false - viewModel.scrollToIndex = nil - } - } - .onChange(of: scrollToTopSignal) { - withAnimation { - proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top) - } + } + .if(canFilterTimeline && !pinnedFilters.isEmpty) { view in + view.toolbarBackground(.hidden, for: .navigationBar) + } + .onChange(of: viewModel.scrollToIndex) { _, newValue in + if let collectionView, + let newValue, + let rows = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0), + rows > newValue + { + collectionView.scrollToItem(at: .init(row: newValue, section: 0), + at: .top, + animated: viewModel.scrollToIndexAnimated) + viewModel.scrollToIndexAnimated = false + viewModel.scrollToIndex = nil } } .toolbar { diff --git a/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift b/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift index 31d544b9..5d8d31d9 100644 --- a/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift +++ b/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift @@ -1,25 +1,31 @@ import Models import Network +import Testing +import Foundation @testable import Timeline -import XCTest -final class TimelineFilterTests: XCTestCase { - func testCodableHome() throws { - XCTAssertTrue(try testCodableOn(filter: .home)) - XCTAssertTrue(try testCodableOn(filter: .local)) - XCTAssertTrue(try testCodableOn(filter: .federated)) - XCTAssertTrue(try testCodableOn(filter: .remoteLocal(server: "me.dm", filter: .local))) - XCTAssertTrue(try testCodableOn(filter: .tagGroup(title: "test", tags: ["test"], symbolName: nil))) - XCTAssertTrue(try testCodableOn(filter: .tagGroup(title: "test", tags: ["test"], symbolName: "test"))) - XCTAssertTrue(try testCodableOn(filter: .hashtag(tag: "test", accountId: nil))) - XCTAssertTrue(try testCodableOn(filter: .list(list: .init(id: "test", title: "test")))) +@Suite("Timeline Filter Tests") +struct TimelineFilterTests { + @Test("All timeline filter can be decoded and encoded", + arguments: [TimelineFilter.home, TimelineFilter.local, TimelineFilter.federated, + TimelineFilter.remoteLocal(server: "me.dm", filter: .local), + TimelineFilter.tagGroup(title: "test", tags: ["test"], symbolName: nil), + TimelineFilter.tagGroup(title: "test", tags: ["test"], symbolName: "test"), + TimelineFilter.hashtag(tag: "test", accountId: nil), + TimelineFilter.list(list: .init(id: "test", title: "test", repliesPolicy: .list, exclusive: true))]) + func timelineCanEncodeAndDecode(filter: TimelineFilter) { + #expect(testCodableOn(filter: filter)) } - private func testCodableOn(filter: TimelineFilter) throws -> Bool { + func testCodableOn(filter: TimelineFilter) -> Bool { let encoder = JSONEncoder() let decoder = JSONDecoder() - let data = try encoder.encode(filter) - let newFilter = try decoder.decode(TimelineFilter.self, from: data) + guard let data = try? encoder.encode(filter) else { + return false + } + let newFilter = try? decoder.decode(TimelineFilter.self, from: data) return newFilter == filter + } + } diff --git a/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift b/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift index 310bd1f4..e0fbfe45 100644 --- a/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift +++ b/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift @@ -2,62 +2,71 @@ import Models import Network @testable import Timeline import XCTest +import Testing @MainActor -final class TimelineViewModelTests: XCTestCase { - var subject = TimelineViewModel() - - override func setUp() async throws { - subject = TimelineViewModel() +@Suite("Timeline View Model tests") +struct Tests { + func makeSubject() -> TimelineViewModel { + let subject = TimelineViewModel() let client = Client(server: "localhost") subject.client = client subject.timeline = .home subject.isTimelineVisible = true subject.timelineTask?.cancel() + return subject } - - func testStreamEventInsertNewStatus() async throws { + + @Test + func streamEventInsertNewStatus() async throws { + let subject = makeSubject() let isEmpty = await subject.datasource.isEmpty - XCTAssertTrue(isEmpty) + #expect(isEmpty) await subject.datasource.append(.placeholder()) var count = await subject.datasource.count() - XCTAssertTrue(count == 1) + #expect(count == 1) await subject.handleEvent(event: StreamEventUpdate(status: .placeholder())) count = await subject.datasource.count() - XCTAssertTrue(count == 2) + #expect(count == 2) } - - func testStreamEventInsertDuplicateStatus() async throws { + + @Test + func streamEventInsertDuplicateStatus() async throws { + let subject = makeSubject() let isEmpty = await subject.datasource.isEmpty - XCTAssertTrue(isEmpty) + #expect(isEmpty) let status = Status.placeholder() await subject.datasource.append(status) var count = await subject.datasource.count() - XCTAssertTrue(count == 1) + #expect(count == 1) await subject.handleEvent(event: StreamEventUpdate(status: status)) count = await subject.datasource.count() - XCTAssertTrue(count == 1) + #expect(count == 1) } - func testStreamEventRemove() async throws { + @Test + func streamEventRemove() async throws { + let subject = makeSubject() let isEmpty = await subject.datasource.isEmpty - XCTAssertTrue(isEmpty) + #expect(isEmpty) let status = Status.placeholder() await subject.datasource.append(status) var count = await subject.datasource.count() - XCTAssertTrue(count == 1) + #expect(count == 1) await subject.handleEvent(event: StreamEventDelete(status: status.id)) count = await subject.datasource.count() - XCTAssertTrue(count == 0) + #expect(count == 0) } - func testStreamEventUpdateStatus() async throws { + @Test + func streamEventUpdateStatus() async throws { + let subject = makeSubject() var status = Status.placeholder() let isEmpty = await subject.datasource.isEmpty - XCTAssertTrue(isEmpty) + #expect(isEmpty) await subject.datasource.append(status) var count = await subject.datasource.count() - XCTAssertTrue(count == 1) + #expect(count == 1) status = .init(id: status.id, content: .init(stringValue: "test"), account: status.account, @@ -88,7 +97,7 @@ final class TimelineViewModelTests: XCTestCase { await subject.handleEvent(event: StreamEventStatusUpdate(status: status)) let statuses = await subject.datasource.get() count = await subject.datasource.count() - XCTAssertTrue(count == 1) - XCTAssertTrue(statuses.first?.content.asRawText == "test") + #expect(count == 1) + #expect(statuses.first?.content.asRawText == "test") } }