This commit is contained in:
Thomas Ricouard 2024-02-14 12:48:14 +01:00
parent 2d988d48c1
commit 1f858414d8
146 changed files with 1610 additions and 1637 deletions

View file

@ -17,29 +17,29 @@ struct AppView: View {
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(StreamWatcher.self) private var watcher @Environment(StreamWatcher.self) private var watcher
@Environment(\.openWindow) var openWindow @Environment(\.openWindow) var openWindow
@Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Binding var selectedTab: Tab @Binding var selectedTab: Tab
@Binding var appRouterPath: RouterPath @Binding var appRouterPath: RouterPath
@State var popToRootTab: Tab = .other @State var popToRootTab: Tab = .other
@State var iosTabs = iOSTabs.shared @State var iosTabs = iOSTabs.shared
@State var sidebarTabs = SidebarTabs.shared @State var sidebarTabs = SidebarTabs.shared
var body: some View { var body: some View {
#if os(visionOS) #if os(visionOS)
tabBarView
#else
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
sidebarView
} else {
tabBarView tabBarView
} #else
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
sidebarView
} else {
tabBarView
}
#endif #endif
} }
var availableTabs: [Tab] { var availableTabs: [Tab] {
guard appAccountsManager.currentClient.isAuth else { guard appAccountsManager.currentClient.isAuth else {
return Tab.loggedOutTab() return Tab.loggedOutTab()
@ -49,7 +49,7 @@ struct AppView: View {
} else if UIDevice.current.userInterfaceIdiom == .vision { } else if UIDevice.current.userInterfaceIdiom == .vision {
return Tab.visionOSTab() return Tab.visionOSTab()
} }
return sidebarTabs.tabs.map{ $0.tab } return sidebarTabs.tabs.map { $0.tab }
} }
var tabBarView: some View { var tabBarView: some View {
@ -58,9 +58,9 @@ struct AppView: View {
}, set: { newTab in }, set: { newTab in
if newTab == .post { if newTab == .post {
#if os(visionOS) #if os(visionOS)
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)) openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
#else #else
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
#endif #endif
return return
} }
@ -104,40 +104,40 @@ struct AppView: View {
} }
return 0 return 0
} }
#if !os(visionOS) #if !os(visionOS)
var sidebarView: some View { var sidebarView: some View {
SideBarView(selectedTab: $selectedTab, SideBarView(selectedTab: $selectedTab,
popToRootTab: $popToRootTab, popToRootTab: $popToRootTab,
tabs: availableTabs) tabs: availableTabs)
{ {
HStack(spacing: 0) { HStack(spacing: 0) {
TabView(selection: $selectedTab) { TabView(selection: $selectedTab) {
ForEach(availableTabs) { tab in ForEach(availableTabs) { tab in
tab tab
.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab) .makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab)
.tabItem { .tabItem {
tab.label tab.label
} }
.tag(tab) .tag(tab)
}
}
.introspect(.tabView, on: .iOS(.v17)) { (tabview: UITabBarController) in
tabview.tabBar.isHidden = horizontalSizeClass == .regular
tabview.customizableViewControllers = []
tabview.moreNavigationController.isNavigationBarHidden = true
}
if horizontalSizeClass == .regular,
appAccountsManager.currentClient.isAuth,
userPreferences.showiPadSecondaryColumn
{
Divider().edgesIgnoringSafeArea(.all)
notificationsSecondaryColumn
} }
} }
.introspect(.tabView, on: .iOS(.v17)) { (tabview: UITabBarController) in
tabview.tabBar.isHidden = horizontalSizeClass == .regular
tabview.customizableViewControllers = []
tabview.moreNavigationController.isNavigationBarHidden = true
}
if horizontalSizeClass == .regular,
appAccountsManager.currentClient.isAuth,
userPreferences.showiPadSecondaryColumn
{
Divider().edgesIgnoringSafeArea(.all)
notificationsSecondaryColumn
}
} }
.environment(appRouterPath)
} }
.environment(appRouterPath)
}
#endif #endif
var notificationsSecondaryColumn: some View { var notificationsSecondaryColumn: some View {

View file

@ -26,10 +26,10 @@ struct IceCubesApp: App {
@State var watcher = StreamWatcher.shared @State var watcher = StreamWatcher.shared
@State var quickLook = QuickLook.shared @State var quickLook = QuickLook.shared
@State var theme = Theme.shared @State var theme = Theme.shared
@State var selectedTab: Tab = .timeline @State var selectedTab: Tab = .timeline
@State var appRouterPath = RouterPath() @State var appRouterPath = RouterPath()
@State var isSupporter: Bool = false @State var isSupporter: Bool = false
var body: some Scene { var body: some Scene {

View file

@ -36,37 +36,37 @@ public struct ReportView: View {
.navigationTitle("report.title") .navigationTitle("report.title")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately) .scrollDismissesKeyboard(.immediately)
#endif #endif
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button { Button {
isSendingReport = true isSendingReport = true
Task { Task {
do { do {
let _: ReportSent = let _: ReportSent =
try await client.post(endpoint: Statuses.report(accountId: status.account.id, try await client.post(endpoint: Statuses.report(accountId: status.account.id,
statusId: status.id, statusId: status.id,
comment: commentText)) comment: commentText))
dismiss() dismiss()
isSendingReport = false isSendingReport = false
} catch { } catch {
isSendingReport = false isSendingReport = false
}
}
} label: {
if isSendingReport {
ProgressView()
} else {
Text("report.action.send")
} }
} }
} label: {
if isSendingReport {
ProgressView()
} else {
Text("report.action.send")
}
} }
}
CancelToolbarItem() CancelToolbarItem()
} }
} }
} }
} }

View file

@ -18,7 +18,7 @@ private struct SafariRouter: ViewModifier {
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
#if !os(visionOS) #if !os(visionOS)
@State private var safariManager = InAppSafariManager() @State private var safariManager = InAppSafariManager()
#endif #endif
func body(content: Content) -> some View { func body(content: Content) -> some View {
@ -52,78 +52,78 @@ private struct SafariRouter: ViewModifier {
return .systemAction return .systemAction
} }
#if os(visionOS) #if os(visionOS)
return .systemAction return .systemAction
#else #else
return safariManager.open(url) return safariManager.open(url)
#endif #endif
#else #else
return .systemAction return .systemAction
#endif #endif
} }
} }
#if !os(visionOS) #if !os(visionOS)
.background { .background {
WindowReader { window in WindowReader { window in
safariManager.windowScene = window.windowScene safariManager.windowScene = window.windowScene
} }
} }
#endif #endif
} }
} }
#if !os(visionOS) #if !os(visionOS)
@MainActor
@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
var windowScene: UIWindowScene?
let viewController: UIViewController = .init()
var window: UIWindow?
@MainActor @MainActor
func open(_ url: URL) -> OpenURLAction.Result { @Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
guard let windowScene else { return .systemAction } var windowScene: UIWindowScene?
let viewController: UIViewController = .init()
var window: UIWindow?
window = setupWindow(windowScene: windowScene) @MainActor
func open(_ url: URL) -> OpenURLAction.Result {
guard let windowScene else { return .systemAction }
let configuration = SFSafariViewController.Configuration() window = setupWindow(windowScene: windowScene)
configuration.entersReaderIfAvailable = UserPreferences.shared.inAppBrowserReaderView
let safari = SFSafariViewController(url: url, configuration: configuration) let configuration = SFSafariViewController.Configuration()
safari.preferredBarTintColor = UIColor(Theme.shared.primaryBackgroundColor) configuration.entersReaderIfAvailable = UserPreferences.shared.inAppBrowserReaderView
safari.preferredControlTintColor = UIColor(Theme.shared.tintColor)
safari.delegate = self
DispatchQueue.main.async { [weak self] in let safari = SFSafariViewController(url: url, configuration: configuration)
self?.viewController.present(safari, animated: true) safari.preferredBarTintColor = UIColor(Theme.shared.primaryBackgroundColor)
safari.preferredControlTintColor = UIColor(Theme.shared.tintColor)
safari.delegate = self
DispatchQueue.main.async { [weak self] in
self?.viewController.present(safari, animated: true)
}
return .handled
} }
return .handled func setupWindow(windowScene: UIWindowScene) -> UIWindow {
} let window = window ?? UIWindow(windowScene: windowScene)
func setupWindow(windowScene: UIWindowScene) -> UIWindow { window.rootViewController = viewController
let window = window ?? UIWindow(windowScene: windowScene) window.makeKeyAndVisible()
window.rootViewController = viewController switch Theme.shared.selectedScheme {
window.makeKeyAndVisible() case .dark:
window.overrideUserInterfaceStyle = .dark
case .light:
window.overrideUserInterfaceStyle = .light
}
switch Theme.shared.selectedScheme { self.window = window
case .dark: return window
window.overrideUserInterfaceStyle = .dark
case .light:
window.overrideUserInterfaceStyle = .light
} }
self.window = window nonisolated func safariViewControllerDidFinish(_: SFSafariViewController) {
return window Task { @MainActor in
} window?.resignKey()
window?.isHidden = false
nonisolated func safariViewControllerDidFinish(_: SFSafariViewController) { window = nil
Task { @MainActor in }
window?.resignKey()
window?.isHidden = false
window = nil
} }
} }
}
#endif #endif
private struct WindowReader: UIViewRepresentable { private struct WindowReader: UIViewRepresentable {

View file

@ -17,12 +17,12 @@ struct SideBarView<Content: View>: View {
@Environment(StreamWatcher.self) private var watcher @Environment(StreamWatcher.self) private var watcher
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@Binding var selectedTab: Tab @Binding var selectedTab: Tab
@Binding var popToRootTab: Tab @Binding var popToRootTab: Tab
var tabs: [Tab] var tabs: [Tab]
@ViewBuilder var content: () -> Content @ViewBuilder var content: () -> Content
@State private var sidebarTabs = SidebarTabs.shared @State private var sidebarTabs = SidebarTabs.shared
private func badgeFor(tab: Tab) -> Int { private func badgeFor(tab: Tab) -> Int {
@ -166,7 +166,7 @@ struct SideBarView<Content: View>: View {
.frame(width: .sidebarWidth) .frame(width: .sidebarWidth)
.background(.thinMaterial) .background(.thinMaterial)
}) })
Divider().edgesIgnoringSafeArea(.all) Divider().edgesIgnoringSafeArea(.all)
} }
content() content()
} }

View file

@ -1,18 +1,18 @@
import SwiftUI
import Env
import AppAccount import AppAccount
import DesignSystem import DesignSystem
import Env
import SwiftUI
@MainActor @MainActor
struct NavigationSheet<Content: View>: View { struct NavigationSheet<Content: View>: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
var content: () -> Content var content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) { init(@ViewBuilder content: @escaping () -> Content) {
self.content = content self.content = content
} }
var body: some View { var body: some View {
NavigationStack { NavigationStack {
content() content()

View file

@ -1,27 +1,27 @@
import SwiftUI
import Env
import AppAccount import AppAccount
import DesignSystem import DesignSystem
import Env
import Network import Network
import SwiftUI
@MainActor @MainActor
struct NavigationTab<Content: View>: View { struct NavigationTab<Content: View>: View {
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool @Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
@Environment(AppAccountsManager.self) private var appAccount @Environment(AppAccountsManager.self) private var appAccount
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(Client.self) private var client @Environment(Client.self) private var client
var content: () -> Content var content: () -> Content
@State private var routerPath = RouterPath() @State private var routerPath = RouterPath()
init(@ViewBuilder content: @escaping () -> Content) { init(@ViewBuilder content: @escaping () -> Content) {
self.content = content self.content = content
} }
var body: some View { var body: some View {
NavigationStack(path: $routerPath.path) { NavigationStack(path: $routerPath.path) {
content() content()

View file

@ -21,7 +21,7 @@ struct NotificationsTab: View {
@Environment(PushNotificationsService.self) private var pushNotificationsService @Environment(PushNotificationsService.self) private var pushNotificationsService
@State private var routerPath = RouterPath() @State private var routerPath = RouterPath()
@State private var scrollToTopSignal: Int = 0 @State private var scrollToTopSignal: Int = 0
@Binding var selectedTab: Tab @Binding var selectedTab: Tab
@Binding var popToRootTab: Tab @Binding var popToRootTab: Tab
@ -60,9 +60,9 @@ struct NotificationsTab: View {
} }
} }
} }
.onChange(of: selectedTab, { _, newValue in .onChange(of: selectedTab) { _, _ in
clearNotifications() clearNotifications()
}) }
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in .onChange(of: pushNotificationsService.handledNotification) { _, newValue in
if let newValue, let type = newValue.notification.supportedType { if let newValue, let type = newValue.notification.supportedType {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {

View file

@ -28,26 +28,26 @@ struct AboutView: View {
List { List {
Section { Section {
#if !targetEnvironment(macCatalyst) && !os(visionOS) #if !targetEnvironment(macCatalyst) && !os(visionOS)
HStack { HStack {
Spacer() Spacer()
Image(uiImage: .init(named: "AppIconAlternate0")!) Image(uiImage: .init(named: "AppIconAlternate0")!)
.resizable() .resizable()
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.cornerRadius(4) .cornerRadius(4)
Image(uiImage: .init(named: "AppIconAlternate4")!) Image(uiImage: .init(named: "AppIconAlternate4")!)
.resizable() .resizable()
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.cornerRadius(4) .cornerRadius(4)
Image(uiImage: .init(named: "AppIconAlternate17")!) Image(uiImage: .init(named: "AppIconAlternate17")!)
.resizable() .resizable()
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.cornerRadius(4) .cornerRadius(4)
Image(uiImage: .init(named: "AppIconAlternate23")!) Image(uiImage: .init(named: "AppIconAlternate23")!)
.resizable() .resizable()
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.cornerRadius(4) .cornerRadius(4)
Spacer() Spacer()
} }
#endif #endif
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) { Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
Label("settings.support.privacy-policy", systemImage: "lock") Label("settings.support.privacy-policy", systemImage: "lock")
@ -107,14 +107,14 @@ struct AboutView: View {
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.navigationTitle(Text("settings.about.title")) .navigationTitle(Text("settings.about.title"))
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
routerPath.handle(url: url) routerPath.handle(url: url)
}) })
} }
@ViewBuilder @ViewBuilder

View file

@ -116,8 +116,8 @@ struct AccountSettingsView: View {
} }
.navigationTitle(account.safeDisplayName) .navigationTitle(account.safeDisplayName)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
} }
} }

View file

@ -82,75 +82,75 @@ struct AddAccountView: View {
.navigationTitle("account.add.navigation-title") .navigationTitle("account.add.navigation-title")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately) .scrollDismissesKeyboard(.immediately)
#endif #endif
.toolbar { .toolbar {
if !appAccountsManager.availableAccounts.isEmpty { if !appAccountsManager.availableAccounts.isEmpty {
CancelToolbarItem() CancelToolbarItem()
}
}
.onAppear {
isInstanceURLFieldFocused = true
Task {
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation {
self.instances = instances
} }
} }
isSigninIn = false .onAppear {
} isInstanceURLFieldFocused = true
.onChange(of: instanceName) { Task {
searchingTask.cancel() let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
searchingTask = Task { withAnimation {
try? await Task.sleep(for: .seconds(0.1)) self.instances = instances
guard !Task.isCancelled else { return } }
}
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName) isSigninIn = false
withAnimation { }
self.instances = instances .onChange(of: instanceName) {
} searchingTask.cancel()
} searchingTask = Task {
try? await Task.sleep(for: .seconds(0.1))
getInstanceDetailTask.cancel() guard !Task.isCancelled else { return }
getInstanceDetailTask = Task {
try? await Task.sleep(for: .seconds(0.1)) let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
guard !Task.isCancelled else { return } withAnimation {
self.instances = instances
do { }
// bare bones preflight for domain validity }
let instanceDetailClient = Client(server: sanitizedName)
if getInstanceDetailTask.cancel()
instanceDetailClient.server.contains("."), getInstanceDetailTask = Task {
instanceDetailClient.server.last != "." try? await Task.sleep(for: .seconds(0.1))
{ guard !Task.isCancelled else { return }
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
withAnimation { do {
self.instance = instance // bare bones preflight for domain validity
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 let instanceDetailClient = Client(server: sanitizedName)
} if
instanceFetchError = nil instanceDetailClient.server.contains("."),
} else { instanceDetailClient.server.last != "."
instance = nil {
instanceFetchError = nil 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
}
instanceFetchError = nil
} else {
instance = nil
instanceFetchError = nil
}
} catch _ as DecodingError {
instance = nil
instanceFetchError = "account.add.error.instance-not-supported"
} catch {
instance = nil
} }
} catch _ as DecodingError {
instance = nil
instanceFetchError = "account.add.error.instance-not-supported"
} catch {
instance = nil
} }
} }
} .onChange(of: scenePhase) { _, newValue in
.onChange(of: scenePhase) { _, newValue in switch newValue {
switch newValue { case .active:
case .active: isSigninIn = false
isSigninIn = false default:
default: break
break }
} }
}
} }
} }
@ -214,9 +214,9 @@ struct AddAccountView: View {
.foregroundColor(.primary) .foregroundColor(.primary)
Spacer() Spacer()
(Text("instance.list.users-\(formatAsNumber(instance.users))") (Text("instance.list.users-\(formatAsNumber(instance.users))")
+ Text("") + Text("")
+ Text("instance.list.posts-\(formatAsNumber(instance.statuses))")) + Text("instance.list.posts-\(formatAsNumber(instance.statuses))"))
.foregroundStyle(theme.tintColor) .foregroundStyle(theme.tintColor)
} }
.padding(.bottom, 5) .padding(.bottom, 5)
Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "") Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "")
@ -263,7 +263,7 @@ struct AddAccountView: View {
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
.allowsHitTesting(false) .allowsHitTesting(false)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }

View file

@ -5,14 +5,14 @@ import Models
import Network import Network
import NukeUI import NukeUI
import SwiftUI import SwiftUI
import UserNotifications
import Timeline import Timeline
import UserNotifications
@MainActor @MainActor
struct ContentSettingsView: View { struct ContentSettingsView: View {
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@State private var contentFilter = TimelineContentFilter.shared @State private var contentFilter = TimelineContentFilter.shared
var body: some View { var body: some View {
@ -41,7 +41,7 @@ struct ContentSettingsView: View {
} }
} }
} }
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
@ -113,7 +113,7 @@ struct ContentSettingsView: View {
Text("settings.content.default-sensitive") Text("settings.content.default-sensitive")
} }
.disabled(userPreferences.useInstanceContentSettings) .disabled(userPreferences.useInstanceContentSettings)
Toggle(isOn: $userPreferences.appRequireAltText) { Toggle(isOn: $userPreferences.appRequireAltText) {
Text("settings.content.require-alt-text") Text("settings.content.require-alt-text")
} }
@ -121,7 +121,7 @@ struct ContentSettingsView: View {
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
Section("timeline.content-filter.title") { Section("timeline.content-filter.title") {
Toggle(isOn: $contentFilter.showBoosts) { Toggle(isOn: $contentFilter.showBoosts) {
Label("timeline.filter.show-boosts", image: "Rocket") Label("timeline.filter.show-boosts", image: "Rocket")
@ -142,8 +142,8 @@ struct ContentSettingsView: View {
} }
.navigationTitle("settings.content.navigation-title") .navigationTitle("settings.content.navigation-title")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
} }
} }

View file

@ -36,11 +36,11 @@ struct DisplaySettingsView: View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
Form { Form {
#if !os(visionOS) #if !os(visionOS)
StatusRowView(viewModel: previewStatusViewModel) StatusRowView(viewModel: previewStatusViewModel)
.allowsHitTesting(false) .allowsHitTesting(false)
.opacity(0) .opacity(0)
.hidden() .hidden()
themeSection themeSection
#endif #endif
fontSection fontSection
layoutSection layoutSection
@ -49,35 +49,35 @@ struct DisplaySettingsView: View {
} }
.navigationTitle("settings.display.navigation-title") .navigationTitle("settings.display.navigation-title")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.task(id: localValues.tintColor) { .task(id: localValues.tintColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {} do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.tintColor = localValues.tintColor theme.tintColor = localValues.tintColor
} }
.task(id: localValues.primaryBackgroundColor) { .task(id: localValues.primaryBackgroundColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {} do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.primaryBackgroundColor = localValues.primaryBackgroundColor theme.primaryBackgroundColor = localValues.primaryBackgroundColor
} }
.task(id: localValues.secondaryBackgroundColor) { .task(id: localValues.secondaryBackgroundColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {} do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor
} }
.task(id: localValues.labelColor) { .task(id: localValues.labelColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch {} do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.labelColor = localValues.labelColor theme.labelColor = localValues.labelColor
} }
.task(id: localValues.lineSpacing) { .task(id: localValues.lineSpacing) {
do { try await Task.sleep(for: .microseconds(500)) } catch {} do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.lineSpacing = localValues.lineSpacing theme.lineSpacing = localValues.lineSpacing
} }
.task(id: localValues.fontSizeScale) { .task(id: localValues.fontSizeScale) {
do { try await Task.sleep(for: .microseconds(500)) } catch {} do { try await Task.sleep(for: .microseconds(500)) } catch {}
theme.fontSizeScale = localValues.fontSizeScale theme.fontSizeScale = localValues.fontSizeScale
} }
#if !os(visionOS) #if !os(visionOS)
examplePost examplePost
#endif #endif
} }
} }

View file

@ -23,8 +23,8 @@ struct HapticSettingsView: View {
} }
.navigationTitle("settings.haptic.navigation-title") .navigationTitle("settings.haptic.navigation-title")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
} }
} }

View file

@ -14,8 +14,8 @@ struct InstanceInfoView: View {
} }
.navigationTitle("instance.info.navigation-title") .navigationTitle("instance.info.navigation-title")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
} }
} }

View file

@ -111,12 +111,12 @@ struct PushNotificationsView: View {
} }
.navigationTitle("settings.push.navigation-title") .navigationTitle("settings.push.navigation-title")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.task { .task {
await subscription.fetchSubscription() await subscription.fetchSubscription()
} }
} }
private func updateSubscription() { private func updateSubscription() {

View file

@ -1,17 +1,17 @@
import SwiftUI
import SwiftData
import Models
import Env
import DesignSystem import DesignSystem
import Env
import Models
import SwiftData
import SwiftUI
struct RecenTagsSettingView: View { struct RecenTagsSettingView: View {
@Environment(\.modelContext) private var context @Environment(\.modelContext) private var context
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Query(sort: \RecentTag.lastUse, order: .reverse) var tags: [RecentTag] @Query(sort: \RecentTag.lastUse, order: .reverse) var tags: [RecentTag]
var body: some View { var body: some View {
Form { Form {
ForEach(tags) { tag in ForEach(tags) { tag in
@ -35,10 +35,10 @@ struct RecenTagsSettingView: View {
.navigationTitle("settings.general.recent-tags") .navigationTitle("settings.general.recent-tags")
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
#if !os(visionOS) #if !os(visionOS)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.toolbar { .toolbar {
EditButton() EditButton()
} }
} }
} }

View file

@ -1,17 +1,17 @@
import SwiftUI
import SwiftData
import Models
import Env
import DesignSystem import DesignSystem
import Env
import Models
import SwiftData
import SwiftUI
struct RemoteTimelinesSettingView: View { struct RemoteTimelinesSettingView: View {
@Environment(\.modelContext) private var context @Environment(\.modelContext) private var context
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline] @Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
var body: some View { var body: some View {
Form { Form {
ForEach(localTimelines) { timeline in ForEach(localTimelines) { timeline in
@ -36,10 +36,10 @@ struct RemoteTimelinesSettingView: View {
.navigationTitle("settings.general.remote-timelines") .navigationTitle("settings.general.remote-timelines")
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
#if !os(visionOS) #if !os(visionOS)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.toolbar { .toolbar {
EditButton() EditButton()
} }
} }
} }

View file

@ -43,27 +43,27 @@ struct SettingsTabs: View {
} }
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
#if !os(visionOS) #if !os(visionOS)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.navigationTitle(Text("settings.title")) .navigationTitle(Text("settings.title"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar) .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
.toolbar { .toolbar {
if isModal { if isModal {
ToolbarItem { ToolbarItem {
Button { Button {
dismiss() dismiss()
} label: { } label: {
Text("action.done").bold() Text("action.done").bold()
}
} }
} }
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal {
SecondaryColumnToolbarItem()
}
} }
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal { .withAppRouter()
SecondaryColumnToolbarItem() .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
}
}
.withAppRouter()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
} }
.onAppear { .onAppear {
routerPath.client = client routerPath.client = client

View file

@ -5,7 +5,7 @@ import SwiftUI
struct SidebarEntriesSettingsView: View { struct SidebarEntriesSettingsView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@State private var sidebarTabs = SidebarTabs.shared @State private var sidebarTabs = SidebarTabs.shared
var body: some View { var body: some View {
@ -28,11 +28,11 @@ struct SidebarEntriesSettingsView: View {
.environment(\.editMode, .constant(.active)) .environment(\.editMode, .constant(.active))
.navigationTitle("settings.general.sidebarEntries") .navigationTitle("settings.general.sidebarEntries")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
} }
func move(from source: IndexSet, to destination: Int) { func move(from source: IndexSet, to destination: Int) {
sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination) sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination)
} }

View file

@ -69,24 +69,24 @@ struct SupportAppView: View {
} }
.navigationTitle("settings.support.navigation-title") .navigationTitle("settings.support.navigation-title")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: { .alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") } Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
}, message: { }, message: {
Text("settings.support.alert.message") Text("settings.support.alert.message")
}) })
.alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: { .alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: {
Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") } Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") }
}, message: { }, message: {
Text("settings.support.alert.error.message") Text("settings.support.alert.error.message")
}) })
.onAppear { .onAppear {
loadingProducts = true loadingProducts = true
fetchStoreProducts() fetchStoreProducts()
refreshUserInfo() refreshUserInfo()
} }
} }
private func purchase(product: StoreProduct) async { private func purchase(product: StoreProduct) async {

View file

@ -49,7 +49,7 @@ struct SwipeActionsSettingsView: View {
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
Section { Section {
Picker(selection: $userPreferences.swipeActionsIconStyle, label: Text("settings.swipeactions.icon-style")) { Picker(selection: $userPreferences.swipeActionsIconStyle, label: Text("settings.swipeactions.icon-style")) {
ForEach(UserPreferences.SwipeActionsIconStyle.allCases, id: \.rawValue) { style in ForEach(UserPreferences.SwipeActionsIconStyle.allCases, id: \.rawValue) { style in
@ -70,8 +70,8 @@ struct SwipeActionsSettingsView: View {
} }
.navigationTitle("settings.swipeactions.navigation-title") .navigationTitle("settings.swipeactions.navigation-title")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
} }

View file

@ -6,7 +6,7 @@ import SwiftUI
struct TabbarEntriesSettingsView: View { struct TabbarEntriesSettingsView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@State private var tabs = iOSTabs.shared @State private var tabs = iOSTabs.shared
var body: some View { var body: some View {
@ -42,7 +42,7 @@ struct TabbarEntriesSettingsView: View {
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
Section { Section {
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel) Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
} }
@ -52,8 +52,8 @@ struct TabbarEntriesSettingsView: View {
} }
.navigationTitle("settings.general.tabbarEntries") .navigationTitle("settings.general.tabbarEntries")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
} }
} }

View file

@ -1,17 +1,17 @@
import SwiftUI
import SwiftData
import Models
import Env
import DesignSystem import DesignSystem
import Env
import Models
import SwiftData
import SwiftUI
struct TagsGroupSettingView: View { struct TagsGroupSettingView: View {
@Environment(\.modelContext) private var context @Environment(\.modelContext) private var context
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup] @Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
var body: some View { var body: some View {
Form { Form {
ForEach(tagGroups) { group in ForEach(tagGroups) { group in
@ -41,10 +41,10 @@ struct TagsGroupSettingView: View {
.navigationTitle("timeline.filter.tag-groups") .navigationTitle("timeline.filter.tag-groups")
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
#if !os(visionOS) #if !os(visionOS)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.toolbar { .toolbar {
EditButton() EditButton()
} }
} }
} }

View file

@ -24,7 +24,7 @@ struct TranslationSettingsView: View {
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
if apiKey.isEmpty { if apiKey.isEmpty {
Section { Section {
Link(destination: URL(string: "https://www.deepl.com/pro-api")!) { Link(destination: URL(string: "https://www.deepl.com/pro-api")!) {
@ -41,13 +41,13 @@ struct TranslationSettingsView: View {
} }
.navigationTitle("settings.translation.navigation-title") .navigationTitle("settings.translation.navigation-title")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.onChange(of: apiKey) { .onChange(of: apiKey) {
writeNewValue() writeNewValue()
} }
.onAppear(perform: updatePrefs) .onAppear(perform: updatePrefs)
} }
@ViewBuilder @ViewBuilder

View file

@ -24,7 +24,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
static func loggedOutTab() -> [Tab] { static func loggedOutTab() -> [Tab] {
[.timeline, .settings] [.timeline, .settings]
} }
static func visionOSTab() -> [Tab] { static func visionOSTab() -> [Tab] {
[.profile, .timeline, .notifications, .mentions, .explore, .post, .settings] [.profile, .timeline, .notifications, .mentions, .explore, .post, .settings]
} }
@ -71,7 +71,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
case .links: case .links:
NavigationTab { TrendingLinksListView(cards: []) } NavigationTab { TrendingLinksListView(cards: []) }
case .post: case .post:
VStack { } VStack {}
case .other: case .other:
EmptyView() EmptyView()
} }
@ -114,7 +114,6 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
Label("explore.section.trending.links", systemImage: iconName) Label("explore.section.trending.links", systemImage: iconName)
case .other: case .other:
EmptyView() EmptyView()
} }
} }
@ -164,7 +163,7 @@ class SidebarTabs {
let tab: Tab let tab: Tab
var enabled: Bool var enabled: Bool
} }
class Storage { class Storage {
@AppStorage("sidebar_tabs") var tabs: [SidedebarTab] = [ @AppStorage("sidebar_tabs") var tabs: [SidedebarTab] = [
.init(tab: .timeline, enabled: true), .init(tab: .timeline, enabled: true),
@ -179,25 +178,25 @@ class SidebarTabs {
.init(tab: .favorites, enabled: true), .init(tab: .favorites, enabled: true),
.init(tab: .followedTags, enabled: true), .init(tab: .followedTags, enabled: true),
.init(tab: .lists, enabled: true), .init(tab: .lists, enabled: true),
.init(tab: .settings, enabled: true), .init(tab: .settings, enabled: true),
.init(tab: .profile, enabled: true), .init(tab: .profile, enabled: true),
] ]
} }
private let storage = Storage() private let storage = Storage()
public static let shared = SidebarTabs() public static let shared = SidebarTabs()
var tabs: [SidedebarTab] { var tabs: [SidedebarTab] {
didSet { didSet {
storage.tabs = tabs storage.tabs = tabs
} }
} }
func isEnabled(_ tab: Tab) -> Bool { func isEnabled(_ tab: Tab) -> Bool {
tabs.first(where: { $0.tab.id == tab.id })?.enabled == true tabs.first(where: { $0.tab.id == tab.id })?.enabled == true
} }
private init() { private init() {
tabs = storage.tabs tabs = storage.tabs
} }
@ -208,7 +207,7 @@ class iOSTabs {
enum TabEntries: String { enum TabEntries: String {
case first, second, third, fourth, fifth case first, second, third, fourth, fifth
} }
class Storage { class Storage {
@AppStorage(TabEntries.first.rawValue) var firstTab = Tab.timeline @AppStorage(TabEntries.first.rawValue) var firstTab = Tab.timeline
@AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications @AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications
@ -216,44 +215,44 @@ class iOSTabs {
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.messages @AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.messages
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile @AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile
} }
private let storage = Storage() private let storage = Storage()
public static let shared = iOSTabs() public static let shared = iOSTabs()
var tabs: [Tab] { var tabs: [Tab] {
[firstTab, secondTab, thirdTab, fourthTab, fifthTab] [firstTab, secondTab, thirdTab, fourthTab, fifthTab]
} }
var firstTab: Tab { var firstTab: Tab {
didSet { didSet {
storage.firstTab = firstTab storage.firstTab = firstTab
} }
} }
var secondTab: Tab { var secondTab: Tab {
didSet { didSet {
storage.secondTab = secondTab storage.secondTab = secondTab
} }
} }
var thirdTab: Tab { var thirdTab: Tab {
didSet { didSet {
storage.thirdTab = thirdTab storage.thirdTab = thirdTab
} }
} }
var fourthTab: Tab { var fourthTab: Tab {
didSet { didSet {
storage.fourthTab = fourthTab storage.fourthTab = fourthTab
} }
} }
var fifthTab: Tab { var fifthTab: Tab {
didSet { didSet {
storage.fifthTab = fifthTab storage.fifthTab = fifthTab
} }
} }
private init() { private init() {
firstTab = storage.firstTab firstTab = storage.firstTab
secondTab = storage.secondTab secondTab = storage.secondTab

View file

@ -61,20 +61,20 @@ struct EditTagGroupView: View {
) )
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.interactively) .scrollDismissesKeyboard(.interactively)
#endif #endif
.toolbar { .toolbar {
CancelToolbarItem() CancelToolbarItem()
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button("action.save", action: { save() }) Button("action.save", action: { save() })
.disabled(!tagGroup.isValid) .disabled(!tagGroup.isValid)
}
}
.onAppear {
focusedField = .title
} }
}
.onAppear {
focusedField = .title
}
} }
} }

View file

@ -52,29 +52,29 @@ struct AddRemoteTimelineView: View {
.navigationTitle("timeline.add-remote.title") .navigationTitle("timeline.add-remote.title")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately) .scrollDismissesKeyboard(.immediately)
#endif #endif
.toolbar { .toolbar {
CancelToolbarItem() CancelToolbarItem()
}
.onChange(of: instanceName) { _, newValue in
instanceNamePublisher.send(newValue)
}
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
Task {
let client = Client(server: newValue)
instance = try? await client.get(endpoint: Instances.instance)
} }
} .onChange(of: instanceName) { _, newValue in
.onAppear { instanceNamePublisher.send(newValue)
isInstanceURLFieldFocused = true }
let client = InstanceSocialClient() .onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
Task { Task {
instances = await client.fetchInstances(keyword: instanceName) let client = Client(server: newValue)
instance = try? await client.get(endpoint: Instances.instance)
}
}
.onAppear {
isInstanceURLFieldFocused = true
let client = InstanceSocialClient()
Task {
instances = await client.fetchInstances(keyword: instanceName)
}
} }
}
} }
} }

View file

@ -95,7 +95,7 @@ struct TimelineTab: View {
} }
switch newValue { switch newValue {
case let .tagGroup(title, _, _): case let .tagGroup(title, _, _):
if let group = tagGroups.first(where: { $0.title == title}) { if let group = tagGroups.first(where: { $0.title == title }) {
selectedTagGroup = group selectedTagGroup = group
} }
default: default:
@ -185,7 +185,7 @@ struct TimelineTab: View {
} }
} }
} }
@ViewBuilder @ViewBuilder
private var headerGroup: some View { private var headerGroup: some View {
ControlGroup { ControlGroup {
@ -209,10 +209,10 @@ struct TimelineTab: View {
pinButton pinButton
} }
} }
@ViewBuilder @ViewBuilder
private var pinButton: some View { private var pinButton: some View {
let index = pinnedFilters.firstIndex(where: { $0.id == timeline.id}) let index = pinnedFilters.firstIndex(where: { $0.id == timeline.id })
Button { Button {
withAnimation { withAnimation {
if let index { if let index {
@ -222,14 +222,14 @@ struct TimelineTab: View {
} }
} }
} label: { } label: {
if index != nil { if index != nil {
Label("status.action.unpin", systemImage: "pin.slash") Label("status.action.unpin", systemImage: "pin.slash")
} else { } else {
Label("status.action.pin", systemImage: "pin") Label("status.action.pin", systemImage: "pin")
} }
} }
} }
private var timelineFiltersButtons: some View { private var timelineFiltersButtons: some View {
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in
Button { Button {
@ -239,7 +239,7 @@ struct TimelineTab: View {
} }
} }
} }
@ViewBuilder @ViewBuilder
private var listsFiltersButons: some View { private var listsFiltersButons: some View {
Menu("timeline.filter.lists") { Menu("timeline.filter.lists") {
@ -257,7 +257,7 @@ struct TimelineTab: View {
} }
} }
} }
@ViewBuilder @ViewBuilder
private var tagsFiltersButtons: some View { private var tagsFiltersButtons: some View {
if !currentAccount.tags.isEmpty { if !currentAccount.tags.isEmpty {
@ -272,7 +272,7 @@ struct TimelineTab: View {
} }
} }
} }
private var localTimelinesFiltersButtons: some View { private var localTimelinesFiltersButtons: some View {
Menu("timeline.filter.local") { Menu("timeline.filter.local") {
ForEach(localTimelines) { remoteLocal in ForEach(localTimelines) { remoteLocal in
@ -291,7 +291,7 @@ struct TimelineTab: View {
} }
} }
} }
private var tagGroupsFiltersButtons: some View { private var tagGroupsFiltersButtons: some View {
Menu("timeline.filter.tag-groups") { Menu("timeline.filter.tag-groups") {
ForEach(tagGroups) { group in ForEach(tagGroups) { group in
@ -312,7 +312,7 @@ struct TimelineTab: View {
} }
} }
} }
private var contentFilterButton: some View { private var contentFilterButton: some View {
Button(action: { Button(action: {
routerPath.presentedSheet = .timelineContentFilter routerPath.presentedSheet = .timelineContentFilter

View file

@ -1,23 +1,24 @@
import SwiftUI
import Env
import AppAccount import AppAccount
import DesignSystem import DesignSystem
import Env
import SwiftUI
@MainActor @MainActor
struct ToolbarTab: ToolbarContent { struct ToolbarTab: ToolbarContent {
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool @Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
@Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@Binding var routerPath: RouterPath @Binding var routerPath: RouterPath
var body: some ToolbarContent { var body: some ToolbarContent {
if !isSecondaryColumn { if !isSecondaryColumn {
statusEditorToolbarItem(routerPath: routerPath, statusEditorToolbarItem(routerPath: routerPath,
visibility: userPreferences.postVisibility) visibility: userPreferences.postVisibility)
if UIDevice.current.userInterfaceIdiom != .pad || if UIDevice.current.userInterfaceIdiom != .pad ||
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact) { (UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
{
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
AppAccountsSelectorView(routerPath: routerPath) AppAccountsSelectorView(routerPath: routerPath)
} }

View file

@ -24,7 +24,7 @@ extension NotificationService {
var _plaintext: Data? var _plaintext: Data?
do { do {
_plaintext = try AES.GCM.open(sealedBox, using: key) _plaintext = try AES.GCM.open(sealedBox, using: key)
} catch { } } catch {}
guard let plaintext = _plaintext else { guard let plaintext = _plaintext else {
return nil return nil
} }

View file

@ -2,11 +2,11 @@ import Account
import AppAccount import AppAccount
import DesignSystem import DesignSystem
import Env import Env
import Models
import Network import Network
import StatusKit import StatusKit
import SwiftUI import SwiftUI
import UIKit import UIKit
import Models
class ShareViewController: UIViewController { class ShareViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {

View file

@ -31,7 +31,7 @@ let package = Package(
.product(name: "Models", package: "Models"), .product(name: "Models", package: "Models"),
.product(name: "StatusKit", package: "StatusKit"), .product(name: "StatusKit", package: "StatusKit"),
.product(name: "Env", package: "Env"), .product(name: "Env", package: "Env"),
.product(name: "ButtonKit", package: "ButtonKit") .product(name: "ButtonKit", package: "ButtonKit"),
], ],
swiftSettings: [ swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"), .enableExperimentalFeature("StrictConcurrency"),

View file

@ -28,16 +28,16 @@ public struct AccountDetailContextMenu: View {
Label("account.action.message", systemImage: "tray.full") Label("account.action.message", systemImage: "tray.full")
} }
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
Divider() Divider()
#endif #endif
if viewModel.relationship?.blocking == true { if viewModel.relationship?.blocking == true {
Button { Button {
Task { Task {
do { do {
viewModel.relationship = try await client.post(endpoint: Accounts.unblock(id: account.id)) viewModel.relationship = try await client.post(endpoint: Accounts.unblock(id: account.id))
} catch { } } catch {}
} }
} label: { } label: {
Label("account.action.unblock", systemImage: "person.crop.circle.badge.exclamationmark") Label("account.action.unblock", systemImage: "person.crop.circle.badge.exclamationmark")
@ -55,7 +55,7 @@ public struct AccountDetailContextMenu: View {
Task { Task {
do { do {
viewModel.relationship = try await client.post(endpoint: Accounts.unmute(id: account.id)) viewModel.relationship = try await client.post(endpoint: Accounts.unmute(id: account.id))
} catch { } } catch {}
} }
} label: { } label: {
Label("account.action.unmute", systemImage: "speaker") Label("account.action.unmute", systemImage: "speaker")
@ -67,7 +67,7 @@ public struct AccountDetailContextMenu: View {
Task { Task {
do { do {
viewModel.relationship = try await client.post(endpoint: Accounts.mute(id: account.id, json: MuteData(duration: duration.rawValue))) viewModel.relationship = try await client.post(endpoint: Accounts.mute(id: account.id, json: MuteData(duration: duration.rawValue)))
} catch { } } catch {}
} }
} }
} }
@ -86,7 +86,7 @@ public struct AccountDetailContextMenu: View {
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
notify: false, notify: false,
reblogs: relationship.showingReblogs)) reblogs: relationship.showingReblogs))
} catch { } } catch {}
} }
} label: { } label: {
Label("account.action.notify-disable", systemImage: "bell.fill") Label("account.action.notify-disable", systemImage: "bell.fill")
@ -98,7 +98,7 @@ public struct AccountDetailContextMenu: View {
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
notify: true, notify: true,
reblogs: relationship.showingReblogs)) reblogs: relationship.showingReblogs))
} catch { } } catch {}
} }
} label: { } label: {
Label("account.action.notify-enable", systemImage: "bell") Label("account.action.notify-enable", systemImage: "bell")
@ -111,7 +111,7 @@ public struct AccountDetailContextMenu: View {
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
notify: relationship.notifying, notify: relationship.notifying,
reblogs: false)) reblogs: false))
} catch { } } catch {}
} }
} label: { } label: {
Label("account.action.reboosts-hide", image: "Rocket.Fill") Label("account.action.reboosts-hide", image: "Rocket.Fill")
@ -123,7 +123,7 @@ public struct AccountDetailContextMenu: View {
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
notify: relationship.notifying, notify: relationship.notifying,
reblogs: true)) reblogs: true))
} catch { } } catch {}
} }
} label: { } label: {
Label("account.action.reboosts-show", image: "Rocket") Label("account.action.reboosts-show", image: "Rocket")
@ -131,9 +131,9 @@ public struct AccountDetailContextMenu: View {
} }
} }
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
Divider() Divider()
#endif #endif
} }
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier { if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier {
@ -164,7 +164,7 @@ public struct AccountDetailContextMenu: View {
} }
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
Divider() Divider()
#endif #endif
} }
} }

View file

@ -372,15 +372,15 @@ struct AccountDetailHeaderView: View {
.accessibilityElement(children: .contain) .accessibilityElement(children: .contain)
.accessibilityLabel("accessibility.tabs.profile.fields.container.label") .accessibilityLabel("accessibility.tabs.profile.fields.container.label")
#if os(visionOS) #if os(visionOS)
.background(Material.thick) .background(Material.thick)
#else #else
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.cornerRadius(4) .cornerRadius(4)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
.stroke(.gray.opacity(0.35), lineWidth: 1) .stroke(.gray.opacity(0.35), lineWidth: 1)
) )
} }
} }
} }

View file

@ -27,7 +27,7 @@ public struct AccountDetailView: View {
@State private var isEditingAccount: Bool = false @State private var isEditingAccount: Bool = false
@State private var isEditingFilters: Bool = false @State private var isEditingFilters: Bool = false
@State private var isEditingRelationshipNote: Bool = false @State private var isEditingRelationshipNote: Bool = false
@State private var displayTitle: Bool = false @State private var displayTitle: Bool = false
@Binding var scrollToTopSignal: Int @Binding var scrollToTopSignal: Int
@ -88,14 +88,14 @@ public struct AccountDetailView: View {
.environment(\.defaultMinListRowHeight, 1) .environment(\.defaultMinListRowHeight, 1)
.listStyle(.plain) .listStyle(.plain)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor) .background(theme.primaryBackgroundColor)
#endif #endif
.onChange(of: scrollToTopSignal) { .onChange(of: scrollToTopSignal) {
withAnimation { withAnimation {
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top) proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
}
} }
}
} }
.onAppear { .onAppear {
guard reasons != .placeholder else { return } guard reasons != .placeholder else { return }
@ -220,7 +220,6 @@ public struct AccountDetailView: View {
AvatarView(account.avatar, config: .badge) AvatarView(account.avatar, config: .badge)
.padding(.leading, -4) .padding(.leading, -4)
.accessibilityLabel(account.safeDisplayName) .accessibilityLabel(account.safeDisplayName)
} }
.accessibilityAddTraits(.isImage) .accessibilityAddTraits(.isImage)
.buttonStyle(.plain) .buttonStyle(.plain)
@ -247,18 +246,18 @@ public struct AccountDetailView: View {
bottom: 0, bottom: 0,
trailing: .layoutPadding)) trailing: .layoutPadding))
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
ForEach(viewModel.pinned) { status in ForEach(viewModel.pinned) { status in
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath)) StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
} }
Rectangle() Rectangle()
#if os(visionOS) #if os(visionOS)
.fill(Color.clear) .fill(Color.clear)
#else #else
.fill(theme.secondaryBackgroundColor) .fill(theme.secondaryBackgroundColor)
#endif #endif
.frame(height: 12) .frame(height: 12)
.listRowInsets(.init()) .listRowInsets(.init())
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
@ -288,7 +287,6 @@ public struct AccountDetailView: View {
routerPath.presentedSheet = .mentionStatusEditor(account: account, routerPath.presentedSheet = .mentionStatusEditor(account: account,
visibility: preferences.postVisibility) visibility: preferences.postVisibility)
#endif #endif
} }
} label: { } label: {
Image(systemName: "arrowshape.turn.up.left") Image(systemName: "arrowshape.turn.up.left")
@ -370,7 +368,7 @@ public struct AccountDetailView: View {
Task { Task {
do { do {
viewModel.relationship = try await client.post(endpoint: Accounts.block(id: account.id)) viewModel.relationship = try await client.post(endpoint: Accounts.block(id: account.id))
} catch { } } catch {}
} }
} }
} }
@ -385,9 +383,9 @@ extension View {
func applyAccountDetailsRowStyle(theme: Theme) -> some View { func applyAccountDetailsRowStyle(theme: Theme) -> some View {
listRowInsets(.init()) listRowInsets(.init())
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }
} }

View file

@ -84,7 +84,7 @@ import SwiftUI
private var tabTask: Task<Void, Never>? private var tabTask: Task<Void, Never>?
private(set) var statuses: [Status] = [] private(set) var statuses: [Status] = []
var boosts: [Status] = [] var boosts: [Status] = []
/// When coming from a URL like a mention tap in a status. /// When coming from a URL like a mention tap in a status.
@ -151,7 +151,7 @@ import SwiftUI
self.familiarFollowers = familiarFollowers?.first?.accounts ?? [] self.familiarFollowers = familiarFollowers?.first?.accounts ?? []
} }
func fetchNewestStatuses(pullToRefresh: Bool) async { func fetchNewestStatuses(pullToRefresh _: Bool) async {
guard let client else { return } guard let client else { return }
do { do {
statusesState = .loading statusesState = .loading
@ -166,7 +166,7 @@ import SwiftUI
pinned: nil)) pinned: nil))
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
if selectedTab == .boosts { if selectedTab == .boosts {
boosts = statuses.filter{ $0.reblog != nil } boosts = statuses.filter { $0.reblog != nil }
} }
if selectedTab == .statuses { if selectedTab == .statuses {
pinned = pinned =
@ -197,17 +197,17 @@ import SwiftUI
case .statuses, .replies, .boosts, .media: case .statuses, .replies, .boosts, .media:
guard let lastId = statuses.last?.id else { return } guard let lastId = statuses.last?.id else { return }
let newStatuses: [Status] = let newStatuses: [Status] =
try await client.get(endpoint: Accounts.statuses(id: accountId, try await client.get(endpoint: Accounts.statuses(id: accountId,
sinceId: lastId, sinceId: lastId,
tag: nil, tag: nil,
onlyMedia: selectedTab == .media, onlyMedia: selectedTab == .media,
excludeReplies: selectedTab != .replies, excludeReplies: selectedTab != .replies,
excludeReblogs: selectedTab != .boosts, excludeReblogs: selectedTab != .boosts,
pinned: nil)) pinned: nil))
statuses.append(contentsOf: newStatuses) statuses.append(contentsOf: newStatuses)
if selectedTab == .boosts { if selectedTab == .boosts {
let newBoosts = statuses.filter{ $0.reblog != nil } let newBoosts = statuses.filter { $0.reblog != nil }
self.boosts.append(contentsOf: newBoosts) boosts.append(contentsOf: newBoosts)
} }
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
if selectedTab == .boosts { if selectedTab == .boosts {
@ -253,7 +253,8 @@ import SwiftUI
if let event = event as? StreamEventUpdate { if let event = event as? StreamEventUpdate {
if event.status.account.id == currentAccount.account?.id { if event.status.account.id == currentAccount.account?.id {
if (event.status.inReplyToId == nil && selectedTab == .statuses) || if (event.status.inReplyToId == nil && selectedTab == .statuses) ||
(event.status.inReplyToId != nil && selectedTab == .replies) { (event.status.inReplyToId != nil && selectedTab == .replies)
{
statuses.insert(event.status, at: 0) statuses.insert(event.status, at: 0)
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
} }

View file

@ -45,7 +45,7 @@ public struct AccountsListView: View {
await viewModel.fetch() await viewModel.fetch()
} }
} }
@ViewBuilder @ViewBuilder
private var listView: some View { private var listView: some View {
if currentAccount.account?.id == viewModel.accountId { if currentAccount.account?.id == viewModel.accountId {
@ -54,7 +54,7 @@ public struct AccountsListView: View {
standardList standardList
} }
} }
private var searchableList: some View { private var searchableList: some View {
List { List {
listContent listContent
@ -74,13 +74,13 @@ public struct AccountsListView: View {
} }
} }
} }
private var standardList: some View { private var standardList: some View {
List { List {
listContent listContent
} }
} }
@ViewBuilder @ViewBuilder
private var listContent: some View { private var listContent: some View {
switch viewModel.state { switch viewModel.state {
@ -89,9 +89,9 @@ public struct AccountsListView: View {
AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder())) AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder()))
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
.allowsHitTesting(false) .allowsHitTesting(false)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }
case let .display(accounts, relationships, nextPageState): case let .display(accounts, relationships, nextPageState):
if case .followers = viewModel.mode, if case .followers = viewModel.mode,
@ -125,9 +125,9 @@ public struct AccountsListView: View {
if let relationship = relationships.first(where: { $0.id == account.id }) { if let relationship = relationships.first(where: { $0.id == account.id }) {
AccountsListRow(viewModel: .init(account: account, AccountsListRow(viewModel: .init(account: account,
relationShip: relationship)) relationShip: relationship))
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }
} }
} }
@ -140,16 +140,16 @@ public struct AccountsListView: View {
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
case .none: case .none:
EmptyView() EmptyView()
} }
case let .error(error): case let .error(error):
Text(error.localizedDescription) Text(error.localizedDescription)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }
} }
} }

View file

@ -1,8 +1,8 @@
import Models import Models
import Network import Network
import Observation import Observation
import SwiftUI
import OSLog import OSLog
import SwiftUI
public enum AccountsListMode { public enum AccountsListMode {
case following(accountId: String), followers(accountId: String) case following(accountId: String), followers(accountId: String)
@ -49,7 +49,7 @@ public enum AccountsListMode {
var state = State.loading var state = State.loading
var totalCount: Int? var totalCount: Int?
var accountId: String? var accountId: String?
var searchQuery: String = "" var searchQuery: String = ""
private var nextPageId: String? private var nextPageId: String?
@ -125,7 +125,7 @@ public enum AccountsListMode {
relationships: relationships, relationships: relationships,
nextPageState: link?.maxId != nil ? .hasNextPage : .none) nextPageState: link?.maxId != nil ? .hasNextPage : .none)
} }
func search() async { func search() async {
guard let client, !searchQuery.isEmpty else { return } guard let client, !searchQuery.isEmpty else { return }
do { do {
@ -144,8 +144,6 @@ public enum AccountsListMode {
relationships: relationships, relationships: relationships,
nextPageState: .none) nextPageState: .none)
} }
} catch { } catch {}
}
} }
} }

View file

@ -2,8 +2,8 @@ import DesignSystem
import Env import Env
import Models import Models
import Network import Network
import SwiftUI
import NukeUI import NukeUI
import SwiftUI
@MainActor @MainActor
public struct EditAccountView: View { public struct EditAccountView: View {
@ -13,8 +13,8 @@ public struct EditAccountView: View {
@Environment(UserPreferences.self) private var userPrefs @Environment(UserPreferences.self) private var userPrefs
@State private var viewModel = EditAccountViewModel() @State private var viewModel = EditAccountViewModel()
public init() { } public init() {}
public var body: some View { public var body: some View {
NavigationStack { NavigationStack {
@ -31,24 +31,24 @@ public struct EditAccountView: View {
} }
.environment(\.editMode, .constant(.active)) .environment(\.editMode, .constant(.active))
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately) .scrollDismissesKeyboard(.immediately)
#endif #endif
.navigationTitle("account.edit.navigation-title") .navigationTitle("account.edit.navigation-title")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
toolbarContent toolbarContent
} }
.alert("account.edit.error.save.title", .alert("account.edit.error.save.title",
isPresented: $viewModel.saveError, isPresented: $viewModel.saveError,
actions: { actions: {
Button("alert.button.ok", action: {}) Button("alert.button.ok", action: {})
}, message: { Text("account.edit.error.save.message") }) }, message: { Text("account.edit.error.save.message") })
.task { .task {
viewModel.client = client viewModel.client = client
await viewModel.fetchAccount() await viewModel.fetchAccount()
} }
} }
} }
@ -64,7 +64,7 @@ public struct EditAccountView: View {
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }
private var imagesSection: some View { private var imagesSection: some View {
Section { Section {
ZStack(alignment: .center) { ZStack(alignment: .center) {

View file

@ -1,9 +1,9 @@
import Models import Models
import Network import Network
import Observation import Observation
import SwiftUI
import PhotosUI import PhotosUI
import StatusKit import StatusKit
import SwiftUI
@MainActor @MainActor
@Observable class EditAccountViewModel { @Observable class EditAccountViewModel {
@ -30,22 +30,23 @@ import StatusKit
var fields: [FieldEditViewModel] = [] var fields: [FieldEditViewModel] = []
var avatar: URL? var avatar: URL?
var header: URL? var header: URL?
var isPhotoPickerPresented: Bool = false { var isPhotoPickerPresented: Bool = false {
didSet { didSet {
if !isPhotoPickerPresented && mediaPickers.isEmpty { if !isPhotoPickerPresented, mediaPickers.isEmpty {
isChangingAvatar = false isChangingAvatar = false
isChangingHeader = false isChangingHeader = false
} }
} }
} }
var isChangingAvatar: Bool = false var isChangingAvatar: Bool = false
var isChangingHeader: Bool = false var isChangingHeader: Bool = false
var isLoading: Bool = true var isLoading: Bool = true
var isSaving: Bool = false var isSaving: Bool = false
var saveError: Bool = false var saveError: Bool = false
var mediaPickers: [PhotosPickerItem] = [] { var mediaPickers: [PhotosPickerItem] = [] {
didSet { didSet {
if let item = mediaPickers.first { if let item = mediaPickers.first {
@ -108,47 +109,47 @@ import StatusKit
saveError = true saveError = true
} }
} }
private func uploadHeader(data: Data) async -> Bool { private func uploadHeader(data: Data) async -> Bool {
guard let client else { return false } guard let client else { return false }
do { do {
let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia, let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia,
version: .v1, version: .v1,
method: "PATCH", method: "PATCH",
mimeType: "image/jpeg", mimeType: "image/jpeg",
filename: "header", filename: "header",
data: data) data: data)
return response?.statusCode == 200 return response?.statusCode == 200
} catch { } catch {
return false return false
} }
} }
private func uploadAvatar(data: Data) async -> Bool { private func uploadAvatar(data: Data) async -> Bool {
guard let client else { return false } guard let client else { return false }
do { do {
let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia, let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia,
version: .v1, version: .v1,
method: "PATCH", method: "PATCH",
mimeType: "image/jpeg", mimeType: "image/jpeg",
filename: "avatar", filename: "avatar",
data: data) data: data)
return response?.statusCode == 200 return response?.statusCode == 200
} catch { } catch {
return false return false
} }
} }
private func getItemImageData(item: PhotosPickerItem) async -> Data? { private func getItemImageData(item: PhotosPickerItem) async -> Data? {
guard let imageFile = try? await item.loadTransferable(type: StatusEditor.ImageFileTranseferable.self) else { return nil } guard let imageFile = try? await item.loadTransferable(type: StatusEditor.ImageFileTranseferable.self) else { return nil }
let compressor = StatusEditor.Compressor() let compressor = StatusEditor.Compressor()
guard let compressedData = await compressor.compressImageFrom(url: imageFile.url), guard let compressedData = await compressor.compressImageFrom(url: imageFile.url),
let image = UIImage(data: compressedData), let image = UIImage(data: compressedData),
let uploadData = try? await compressor.compressImageForUpload(image) let uploadData = try? await compressor.compressImageForUpload(image)
else { return nil } else { return nil }
return uploadData return uploadData
} }
} }

View file

@ -71,20 +71,20 @@ struct EditFilterView: View {
.navigationTitle(filter?.title ?? NSLocalizedString("filter.new", comment: "")) .navigationTitle(filter?.title ?? NSLocalizedString("filter.new", comment: ""))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.scrollDismissesKeyboard(.interactively) .scrollDismissesKeyboard(.interactively)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.onAppear { .onAppear {
if filter == nil { if filter == nil {
focusedField = .title focusedField = .title
}
} }
} .toolbar {
.toolbar { ToolbarItem(placement: .navigationBarTrailing) {
ToolbarItem(placement: .navigationBarTrailing) { saveButton
saveButton }
} }
}
} }
private var expirySection: some View { private var expirySection: some View {

View file

@ -74,18 +74,18 @@ public struct FiltersListView: View {
.navigationTitle("filter.filters") .navigationTitle("filter.filters")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.task { .task {
do { do {
isLoading = true isLoading = true
filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2) filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2)
isLoading = false isLoading = false
} catch { } catch {
isLoading = false isLoading = false
}
} }
}
} }
} }

View file

@ -4,8 +4,8 @@ import Foundation
import Models import Models
import Network import Network
import Observation import Observation
import SwiftUI
import OSLog import OSLog
import SwiftUI
@MainActor @MainActor
@Observable public class FollowButtonViewModel { @Observable public class FollowButtonViewModel {

View file

@ -1,14 +1,14 @@
import DesignSystem import DesignSystem
import Env
import Models import Models
import SwiftUI import SwiftUI
import Env
public struct ListsListView: View { public struct ListsListView: View {
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
public init() {} public init() {}
public var body: some View { public var body: some View {
List { List {
ForEach(currentAccount.lists) { list in ForEach(currentAccount.lists) { list in
@ -43,4 +43,3 @@ public struct ListsListView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
} }

View file

@ -1,50 +1,50 @@
import StatusKit import DesignSystem
import Network
import SwiftUI
import Env import Env
import Models import Models
import DesignSystem import Network
import StatusKit
import SwiftUI
@MainActor @MainActor
public struct AccountStatusesListView: View { public struct AccountStatusesListView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(Client.self) private var client @Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@State private var viewModel: AccountStatusesListViewModel @State private var viewModel: AccountStatusesListViewModel
@State private var isLoaded = false @State private var isLoaded = false
public init(mode: AccountStatusesListViewModel.Mode) { public init(mode: AccountStatusesListViewModel.Mode) {
_viewModel = .init(initialValue: .init(mode: mode)) _viewModel = .init(initialValue: .init(mode: mode))
} }
public var body: some View { public var body: some View {
List { List {
StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath) StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath)
} }
.listStyle(.plain) .listStyle(.plain)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor) .background(theme.primaryBackgroundColor)
#endif #endif
.navigationTitle(viewModel.mode.title) .navigationTitle(viewModel.mode.title)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.refreshable { .refreshable {
await viewModel.fetchNewestStatuses(pullToRefresh: true) await viewModel.fetchNewestStatuses(pullToRefresh: true)
} }
.task { .task {
guard !isLoaded else { return } guard !isLoaded else { return }
viewModel.client = client viewModel.client = client
await viewModel.fetchNewestStatuses(pullToRefresh: false)
isLoaded = true
}
.onChange(of: client.id) { _, _ in
isLoaded = false
viewModel.client = client
Task {
await viewModel.fetchNewestStatuses(pullToRefresh: false) await viewModel.fetchNewestStatuses(pullToRefresh: false)
isLoaded = true isLoaded = true
} }
} .onChange(of: client.id) { _, _ in
isLoaded = false
viewModel.client = client
Task {
await viewModel.fetchNewestStatuses(pullToRefresh: false)
isLoaded = true
}
}
} }
} }

View file

@ -1,15 +1,15 @@
import SwiftUI
import Models
import StatusKit
import Network
import Env import Env
import Models
import Network
import StatusKit
import SwiftUI
@MainActor @MainActor
@Observable @Observable
public class AccountStatusesListViewModel: StatusesFetcher { public class AccountStatusesListViewModel: StatusesFetcher {
public enum Mode { public enum Mode {
case bookmarks, favorites case bookmarks, favorites
var title: LocalizedStringKey { var title: LocalizedStringKey {
switch self { switch self {
case .bookmarks: case .bookmarks:
@ -18,7 +18,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
"accessibility.tabs.profile.picker.favorites" "accessibility.tabs.profile.picker.favorites"
} }
} }
func endpoint(sinceId: String?) -> Endpoint { func endpoint(sinceId: String?) -> Endpoint {
switch self { switch self {
case .bookmarks: case .bookmarks:
@ -28,19 +28,19 @@ public class AccountStatusesListViewModel: StatusesFetcher {
} }
} }
} }
let mode: Mode let mode: Mode
public var statusesState: StatusesState = .loading public var statusesState: StatusesState = .loading
var statuses: [Status] = [] var statuses: [Status] = []
var nextPage: LinkHandler? var nextPage: LinkHandler?
var client: Client? var client: Client?
init(mode: Mode) { init(mode: Mode) {
self.mode = mode self.mode = mode
} }
public func fetchNewestStatuses(pullToRefresh: Bool) async { public func fetchNewestStatuses(pullToRefresh _: Bool) async {
guard let client else { return } guard let client else { return }
statusesState = .loading statusesState = .loading
do { do {
@ -52,7 +52,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
statusesState = .error(error: error) statusesState = .error(error: error)
} }
} }
public func fetchNextPage() async throws { public func fetchNextPage() async throws {
guard let client, let nextId = nextPage?.maxId else { return } guard let client, let nextId = nextPage?.maxId else { return }
var newStatuses: [Status] = [] var newStatuses: [Status] = []
@ -62,12 +62,8 @@ public class AccountStatusesListViewModel: StatusesFetcher {
statusesState = .display(statuses: statuses, statusesState = .display(statuses: statuses,
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none) nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
} }
public func statusDidAppear(status: Status) { public func statusDidAppear(status _: Status) {}
} public func statusDidDisappear(status _: Status) {}
public func statusDidDisappear(status: Status) {
}
} }

View file

@ -1,20 +1,20 @@
import DesignSystem import DesignSystem
import Env
import Models import Models
import SwiftUI import SwiftUI
import Env
public struct FollowedTagsListView: View { public struct FollowedTagsListView: View {
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
public init() {} public init() {}
public var body: some View { public var body: some View {
List(currentAccount.tags) { tag in List(currentAccount.tags) { tag in
TagRowView(tag: tag) TagRowView(tag: tag)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
.padding(.vertical, 4) .padding(.vertical, 4)
} }
.task { .task {
@ -32,4 +32,3 @@ public struct FollowedTagsListView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
} }

View file

@ -13,7 +13,7 @@ public struct AppAccountView: View {
@State var viewModel: AppAccountViewModel @State var viewModel: AppAccountViewModel
@Binding var isParentPresented: Bool @Binding var isParentPresented: Bool
public init(viewModel: AppAccountViewModel, isParentPresented: Binding<Bool>) { public init(viewModel: AppAccountViewModel, isParentPresented: Binding<Bool>) {
self.viewModel = viewModel self.viewModel = viewModel
_isParentPresented = isParentPresented _isParentPresented = isParentPresented

View file

@ -97,11 +97,11 @@ public struct AppAccountsSelectorView: View {
} }
addAccountButton addAccountButton
#if os(visionOS) #if os(visionOS)
.foregroundStyle(theme.labelColor) .foregroundStyle(theme.labelColor)
#endif #endif
} }
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
if accountCreationEnabled { if accountCreationEnabled {
@ -134,7 +134,7 @@ public struct AppAccountsSelectorView: View {
.environment(routerPath) .environment(routerPath)
} }
} }
private var addAccountButton: some View { private var addAccountButton: some View {
Button { Button {
isPresented = false isPresented = false
@ -158,7 +158,7 @@ public struct AppAccountsSelectorView: View {
Label("tab.settings", systemImage: "gear") Label("tab.settings", systemImage: "gear")
} }
} }
private var supportButton: some View { private var supportButton: some View {
Button { Button {
isPresented = false isPresented = false
@ -170,7 +170,7 @@ public struct AppAccountsSelectorView: View {
Label("settings.app.support", systemImage: "wand.and.stars") Label("settings.app.support", systemImage: "wand.and.stars")
} }
} }
private var aboutButton: some View { private var aboutButton: some View {
Button { Button {
isPresented = false isPresented = false

View file

@ -71,35 +71,35 @@ public struct ConversationDetailView: View {
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor) .background(theme.primaryBackgroundColor)
#endif #endif
.toolbar { .toolbar {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
if viewModel.conversation.accounts.count == 1, if viewModel.conversation.accounts.count == 1,
let account = viewModel.conversation.accounts.first let account = viewModel.conversation.accounts.first
{ {
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis) EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
.font(.scaledHeadline) .font(.scaledHeadline)
.foregroundColor(theme.labelColor) .foregroundColor(theme.labelColor)
.emojiText.size(Font.scaledHeadlineFont.emojiSize) .emojiText.size(Font.scaledHeadlineFont.emojiSize)
.emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset) .emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
} else { } else {
Text("Direct message with \(viewModel.conversation.accounts.count) people") Text("Direct message with \(viewModel.conversation.accounts.count) people")
.font(.scaledHeadline) .font(.scaledHeadline)
} }
} }
} }
.onChange(of: watcher.latestEvent?.id) { .onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent { if let latestEvent = watcher.latestEvent {
viewModel.handleEvent(event: latestEvent) viewModel.handleEvent(event: latestEvent)
DispatchQueue.main.async { DispatchQueue.main.async {
withAnimation { withAnimation {
scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom) scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom)
}
} }
} }
} }
}
} }
private var loadingView: some View { private var loadingView: some View {

View file

@ -205,12 +205,12 @@ struct ConversationMessageView: View {
.frame(height: 200) .frame(height: 200)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
#if targetEnvironment(macCatalyst) || os(visionOS) #if targetEnvironment(macCatalyst) || os(visionOS)
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement], openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement],
selectedAttachment: attachement)) selectedAttachment: attachement))
#else #else
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement]) quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
#endif #endif
} }
} }

View file

@ -7,7 +7,7 @@ import SwiftUI
@MainActor @MainActor
struct ConversationsListRow: View { struct ConversationsListRow: View {
@Environment(\.openWindow) private var openWindow @Environment(\.openWindow) private var openWindow
@Environment(Client.self) private var client @Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme

View file

@ -201,4 +201,3 @@ public struct ThreadsLight: ColorSet {
public init() {} public init() {}
} }

View file

@ -1,11 +1,11 @@
import SwiftUI import SwiftUI
public extension View { public extension View {
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View { @ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition { if condition {
transform(self) transform(self)
} else { } else {
self self
}
} }
}
} }

View file

@ -5,11 +5,11 @@ import UIKit
public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable { public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
public var window: UIWindow? public var window: UIWindow?
#if os(visionOS) #if os(visionOS)
public private(set) var windowWidth: CGFloat = 0 public private(set) var windowWidth: CGFloat = 0
public private(set) var windowHeight: CGFloat = 0 public private(set) var windowHeight: CGFloat = 0
#else #else
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height
#endif #endif
public func scene(_ scene: UIScene, public func scene(_ scene: UIScene,
@ -30,11 +30,11 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
override public init() { override public init() {
super.init() super.init()
#if os(visionOS) #if os(visionOS)
windowWidth = window?.bounds.size.width ?? 0 windowWidth = window?.bounds.size.width ?? 0
windowHeight = window?.bounds.size.height ?? 0 windowHeight = window?.bounds.size.height ?? 0
#else #else
windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width
windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height
#endif #endif
Self.observedSceneDelegate.insert(self) Self.observedSceneDelegate.insert(self)
_ = Self.observer // just for activating the lazy static property _ = Self.observer // just for activating the lazy static property
@ -52,25 +52,24 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
try? await Task.sleep(for: .seconds(0.1)) try? await Task.sleep(for: .seconds(0.1))
for delegate in observedSceneDelegate { for delegate in observedSceneDelegate {
#if os(visionOS) #if os(visionOS)
let newWidth = delegate.window?.bounds.size.width ?? 0 let newWidth = delegate.window?.bounds.size.width ?? 0
if delegate.windowWidth != newWidth { if delegate.windowWidth != newWidth {
delegate.windowWidth = newWidth delegate.windowWidth = newWidth
} }
let newHeight = delegate.window?.bounds.size.height ?? 0 let newHeight = delegate.window?.bounds.size.height ?? 0
if delegate.windowHeight != newHeight { if delegate.windowHeight != newHeight {
delegate.windowHeight = newHeight delegate.windowHeight = newHeight
} }
#else #else
let newWidth = delegate.window?.bounds.size.width ?? UIScreen.main.bounds.size.width let newWidth = delegate.window?.bounds.size.width ?? UIScreen.main.bounds.size.width
if delegate.windowWidth != newWidth { if delegate.windowWidth != newWidth {
delegate.windowWidth = newWidth delegate.windowWidth = newWidth
} }
let newHeight = delegate.window?.bounds.size.height ?? UIScreen.main.bounds.size.height let newHeight = delegate.window?.bounds.size.height ?? UIScreen.main.bounds.size.height
if delegate.windowHeight != newHeight { if delegate.windowHeight != newHeight {
delegate.windowHeight = newHeight delegate.windowHeight = newHeight
} }
#endif #endif
} }
} }
} }

View file

@ -69,10 +69,10 @@ import SwiftUI
} }
} }
} }
public enum StatusActionSecondary: String, CaseIterable { public enum StatusActionSecondary: String, CaseIterable {
case share, bookmark case share, bookmark
public var description: LocalizedStringKey { public var description: LocalizedStringKey {
switch self { switch self {
case .share: case .share:
@ -197,7 +197,7 @@ import SwiftUI
// better against the tintColor // better against the tintColor
private func computeContrastingTintColor() { private func computeContrastingTintColor() {
func luminance(_ color: Color.Resolved) -> Float { func luminance(_ color: Color.Resolved) -> Float {
return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue; return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue
} }
let resolvedTintColor = tintColor.resolve(in: .init()) let resolvedTintColor = tintColor.resolve(in: .init())
@ -244,7 +244,7 @@ import SwiftUI
themeStorage.statusDisplayStyle = statusDisplayStyle themeStorage.statusDisplayStyle = statusDisplayStyle
} }
} }
public var statusActionSecondary: StatusActionSecondary { public var statusActionSecondary: StatusActionSecondary {
didSet { didSet {
themeStorage.statusActionSecondary = statusActionSecondary themeStorage.statusActionSecondary = statusActionSecondary
@ -300,7 +300,7 @@ import SwiftUI
chosenFontData = nil chosenFontData = nil
statusActionSecondary = .share statusActionSecondary = .share
} }
private init() { private init() {
isThemePreviouslySet = themeStorage.isThemePreviouslySet isThemePreviouslySet = themeStorage.isThemePreviouslySet
selectedScheme = themeStorage.selectedScheme selectedScheme = themeStorage.selectedScheme
@ -340,7 +340,7 @@ import SwiftUI
ConstellationLight(), ConstellationLight(),
ConstellationDark(), ConstellationDark(),
ThreadsLight(), ThreadsLight(),
ThreadsDark() ThreadsDark(),
] ]
} }

View file

@ -77,17 +77,15 @@ struct ThemeApplier: ViewModifier {
} }
private func setWindowUserInterfaceStyle(_ userInterfaceStyle: UIUserInterfaceStyle) { private func setWindowUserInterfaceStyle(_ userInterfaceStyle: UIUserInterfaceStyle) {
allWindows() for window in allWindows() {
.forEach { window.overrideUserInterfaceStyle = userInterfaceStyle
$0.overrideUserInterfaceStyle = userInterfaceStyle }
}
} }
private func setWindowTint(_ color: Color) { private func setWindowTint(_ color: Color) {
allWindows() for window in allWindows() {
.forEach { window.tintColor = UIColor(color)
$0.tintColor = UIColor(color) }
}
} }
private func setBarsColor(_ color: Color) { private func setBarsColor(_ color: Color) {

View file

@ -2,9 +2,9 @@ import SwiftUI
public struct CancelToolbarItem: ToolbarContent { public struct CancelToolbarItem: ToolbarContent {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
public init() { } public init() {}
public var body: some ToolbarContent { public var body: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("action.cancel", role: .cancel, action: { dismiss() }) Button("action.cancel", role: .cancel, action: { dismiss() })

View file

@ -4,12 +4,12 @@ public struct NextPageView: View {
@State private var isLoadingNextPage: Bool = false @State private var isLoadingNextPage: Bool = false
@State private var showRetry: Bool = false @State private var showRetry: Bool = false
let loadNextPage: (() async throws -> Void) let loadNextPage: () async throws -> Void
public init(loadNextPage: @escaping (() async throws -> Void)) { public init(loadNextPage: @escaping (() async throws -> Void)) {
self.loadNextPage = loadNextPage self.loadNextPage = loadNextPage
} }
public var body: some View { public var body: some View {
HStack { HStack {
if showRetry { if showRetry {
@ -35,7 +35,7 @@ public struct NextPageView: View {
} }
.listRowSeparator(.hidden, edges: .all) .listRowSeparator(.hidden, edges: .all)
} }
private func executeTask() async { private func executeTask() async {
showRetry = false showRetry = false
defer { defer {

View file

@ -56,12 +56,12 @@ public extension EnvironmentValues {
get { self[IsCompact.self] } get { self[IsCompact.self] }
set { self[IsCompact.self] = newValue } set { self[IsCompact.self] = newValue }
} }
var isMediaCompact: Bool { var isMediaCompact: Bool {
get { self[IsMediaCompact.self] } get { self[IsMediaCompact.self] }
set { self[IsMediaCompact.self] = newValue } set { self[IsMediaCompact.self] = newValue }
} }
var isModal: Bool { var isModal: Bool {
get { self[IsModal.self] } get { self[IsModal.self] }
set { self[IsModal.self] = newValue } set { self[IsModal.self] = newValue }

View file

@ -6,65 +6,65 @@ public class HapticManager {
public static let shared: HapticManager = .init() public static let shared: HapticManager = .init()
#if os(visionOS) #if os(visionOS)
public enum FeedbackType: Int { public enum FeedbackType: Int {
case success, warning, error case success, warning, error
} }
#endif #endif
public enum HapticType { public enum HapticType {
case buttonPress case buttonPress
case dataRefresh(intensity: CGFloat) case dataRefresh(intensity: CGFloat)
#if os(visionOS) #if os(visionOS)
case notification(_ type: FeedbackType) case notification(_ type: FeedbackType)
#else #else
case notification(_ type: UINotificationFeedbackGenerator.FeedbackType) case notification(_ type: UINotificationFeedbackGenerator.FeedbackType)
#endif #endif
case tabSelection case tabSelection
case timeline case timeline
} }
#if !os(visionOS) #if !os(visionOS)
private let selectionGenerator = UISelectionFeedbackGenerator() private let selectionGenerator = UISelectionFeedbackGenerator()
private let impactGenerator = UIImpactFeedbackGenerator(style: .heavy) private let impactGenerator = UIImpactFeedbackGenerator(style: .heavy)
private let notificationGenerator = UINotificationFeedbackGenerator() private let notificationGenerator = UINotificationFeedbackGenerator()
#endif #endif
private let userPreferences = UserPreferences.shared private let userPreferences = UserPreferences.shared
private init() { private init() {
#if !os(visionOS) #if !os(visionOS)
selectionGenerator.prepare() selectionGenerator.prepare()
impactGenerator.prepare() impactGenerator.prepare()
#endif #endif
} }
@MainActor @MainActor
public func fireHaptic(_ type: HapticType) { public func fireHaptic(_ type: HapticType) {
#if !os(visionOS) #if !os(visionOS)
guard supportsHaptics else { return } guard supportsHaptics else { return }
switch type { switch type {
case .buttonPress: case .buttonPress:
if userPreferences.hapticButtonPressEnabled { if userPreferences.hapticButtonPressEnabled {
impactGenerator.impactOccurred() impactGenerator.impactOccurred()
}
case let .dataRefresh(intensity):
if userPreferences.hapticTimelineEnabled {
impactGenerator.impactOccurred(intensity: intensity)
}
case let .notification(type):
if userPreferences.hapticButtonPressEnabled {
notificationGenerator.notificationOccurred(type)
}
case .tabSelection:
if userPreferences.hapticTabSelectionEnabled {
selectionGenerator.selectionChanged()
}
case .timeline:
if userPreferences.hapticTimelineEnabled {
selectionGenerator.selectionChanged()
}
} }
case let .dataRefresh(intensity):
if userPreferences.hapticTimelineEnabled {
impactGenerator.impactOccurred(intensity: intensity)
}
case let .notification(type):
if userPreferences.hapticButtonPressEnabled {
notificationGenerator.notificationOccurred(type)
}
case .tabSelection:
if userPreferences.hapticTabSelectionEnabled {
selectionGenerator.selectionChanged()
}
case .timeline:
if userPreferences.hapticTimelineEnabled {
selectionGenerator.selectionChanged()
}
}
#endif #endif
} }

View file

@ -64,7 +64,7 @@ public enum SheetDestination: Identifiable {
public var id: String { public var id: String {
switch self { switch self {
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor, case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
.mentionStatusEditor, .quoteLinkStatusEditor: .mentionStatusEditor, .quoteLinkStatusEditor:
"statusEditor" "statusEditor"
case .listCreate: case .listCreate:
"listCreate" "listCreate"
@ -147,8 +147,9 @@ public enum SheetDestination: Identifiable {
navigate(to: .hashTag(tag: tag, account: nil)) navigate(to: .hashTag(tag: tag, account: nil))
return .handled return .handled
} else if url.lastPathComponent.first == "@", } else if url.lastPathComponent.first == "@",
let host = url.host, let host = url.host,
!host.hasPrefix("www") { !host.hasPrefix("www")
{
let acct = "\(url.lastPathComponent)@\(host)" let acct = "\(url.lastPathComponent)@\(host)"
Task { Task {
await navigateToAccountFrom(acct: acct, url: url) await navigateToAccountFrom(acct: acct, url: url)

View file

@ -27,11 +27,11 @@ import OSLog
public var events: [any StreamEvent] = [] public var events: [any StreamEvent] = []
public var unreadNotificationsCount: Int = 0 public var unreadNotificationsCount: Int = 0
public var latestEvent: (any StreamEvent)? public var latestEvent: (any StreamEvent)?
private let logger = Logger(subsystem: "com.icecubesapp", category: "stream") private let logger = Logger(subsystem: "com.icecubesapp", category: "stream")
public static let shared = StreamWatcher() public static let shared = StreamWatcher()
private init() { private init() {
decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.keyDecodingStrategy = .convertFromSnakeCase
} }
@ -65,7 +65,7 @@ import OSLog
connect() connect()
} }
watchedStreams = streams watchedStreams = streams
streams.forEach { stream in for stream in streams {
sendMessage(message: StreamMessage(type: "subscribe", stream: stream.rawValue)) sendMessage(message: StreamMessage(type: "subscribe", stream: stream.rawValue))
} }
} }
@ -156,22 +156,22 @@ import OSLog
return nil return nil
} }
} }
public func emmitDeleteEvent(for status: String) { public func emmitDeleteEvent(for status: String) {
let event = StreamEventDelete(status: status) let event = StreamEventDelete(status: status)
self.events.append(event) events.append(event)
self.latestEvent = event latestEvent = event
} }
public func emmitEditEvent(for status: Status) { public func emmitEditEvent(for status: Status) {
let event = StreamEventStatusUpdate(status: status) let event = StreamEventStatusUpdate(status: status)
self.events.append(event) events.append(event)
self.latestEvent = event latestEvent = event
} }
public func emmitPostEvent(for status: Status) { public func emmitPostEvent(for status: Status) {
let event = StreamEventUpdate(status: status) let event = StreamEventUpdate(status: status)
self.events.append(event) events.append(event)
self.latestEvent = event latestEvent = event
} }
} }

View file

@ -53,7 +53,7 @@ import SwiftUI
@AppStorage("collapse-long-posts") public var collapseLongPosts = true @AppStorage("collapse-long-posts") public var collapseLongPosts = true
@AppStorage("share-button-behavior") public var shareButtonBehavior: PreferredShareButtonBehavior = .linkOnly @AppStorage("share-button-behavior") public var shareButtonBehavior: PreferredShareButtonBehavior = .linkOnly
@AppStorage("fast_refresh") public var fastRefreshEnabled: Bool = false @AppStorage("fast_refresh") public var fastRefreshEnabled: Bool = false
@AppStorage("max_reply_indentation") public var maxReplyIndentation: UInt = 7 @AppStorage("max_reply_indentation") public var maxReplyIndentation: UInt = 7
@ -164,7 +164,7 @@ import SwiftUI
storage.appDefaultPostsSensitive = appDefaultPostsSensitive storage.appDefaultPostsSensitive = appDefaultPostsSensitive
} }
} }
public var appRequireAltText: Bool { public var appRequireAltText: Bool {
didSet { didSet {
storage.appRequireAltText = appRequireAltText storage.appRequireAltText = appRequireAltText
@ -183,7 +183,6 @@ import SwiftUI
} }
} }
public var alwaysUseDeepl: Bool { public var alwaysUseDeepl: Bool {
didSet { didSet {
storage.alwaysUseDeepl = alwaysUseDeepl storage.alwaysUseDeepl = alwaysUseDeepl
@ -303,7 +302,7 @@ import SwiftUI
storage.shareButtonBehavior = shareButtonBehavior storage.shareButtonBehavior = shareButtonBehavior
} }
} }
public var fastRefreshEnabled: Bool { public var fastRefreshEnabled: Bool {
didSet { didSet {
storage.fastRefreshEnabled = fastRefreshEnabled storage.fastRefreshEnabled = fastRefreshEnabled
@ -415,7 +414,7 @@ import SwiftUI
} }
public var totalNotificationsCount: Int { public var totalNotificationsCount: Int {
notificationsCount.compactMap{ $0.value }.reduce(0, +) notificationsCount.compactMap { $0.value }.reduce(0, +)
} }
public func reloadNotificationsCount(tokens: [OauthToken]) { public func reloadNotificationsCount(tokens: [OauthToken]) {
@ -509,7 +508,7 @@ extension UInt: RawRepresentable {
public var rawValue: Int { public var rawValue: Int {
Int(self) Int(self)
} }
public init?(rawValue: Int) { public init?(rawValue: Int) {
if rawValue >= 0 { if rawValue >= 0 {
self.init(rawValue) self.init(rawValue)

View file

@ -1,7 +1,7 @@
@testable import Env @testable import Env
import XCTest
import SwiftUI
import Network import Network
import SwiftUI
import XCTest
@MainActor @MainActor
final class RouterTests: XCTestCase { final class RouterTests: XCTestCase {
@ -11,14 +11,14 @@ final class RouterTests: XCTestCase {
_ = router.handle(url: url) _ = router.handle(url: url)
XCTAssertTrue(router.path.isEmpty) XCTAssertTrue(router.path.isEmpty)
} }
func testRouterTagsURL() { func testRouterTagsURL() {
let router = RouterPath() let router = RouterPath()
let url = URL(string: "https://mastodon.social/tags/test")! let url = URL(string: "https://mastodon.social/tags/test")!
_ = router.handle(url: url) _ = router.handle(url: url)
XCTAssertTrue(router.path.first == .hashTag(tag: "test", account: nil)) XCTAssertTrue(router.path.first == .hashTag(tag: "test", account: nil))
} }
func testRouterLocalStatusURL() { func testRouterLocalStatusURL() {
let router = RouterPath() let router = RouterPath()
let client = Client(server: "mastodon.social", let client = Client(server: "mastodon.social",
@ -29,7 +29,7 @@ final class RouterTests: XCTestCase {
_ = router.handle(url: url) _ = router.handle(url: url)
XCTAssertTrue(router.path.first == .statusDetail(id: "1010384")) XCTAssertTrue(router.path.first == .statusDetail(id: "1010384"))
} }
func testRouterRemoteStatusURL() { func testRouterRemoteStatusURL() {
let router = RouterPath() let router = RouterPath()
let client = Client(server: "mastodon.social", let client = Client(server: "mastodon.social",
@ -40,7 +40,7 @@ final class RouterTests: XCTestCase {
_ = router.handle(url: url) _ = router.handle(url: url)
XCTAssertTrue(router.path.first == .remoteStatusDetail(url: url)) XCTAssertTrue(router.path.first == .remoteStatusDetail(url: url))
} }
func testRouteRandomURL() { func testRouteRandomURL() {
let router = RouterPath() let router = RouterPath()
let url = URL(string: "https://theweb.com/test/test/one")! let url = URL(string: "https://theweb.com/test/test/one")!

View file

@ -56,9 +56,9 @@ public struct ExploreView: View {
EmptyView(iconName: "magnifyingglass", EmptyView(iconName: "magnifyingglass",
title: "explore.search.title", title: "explore.search.title",
message: "explore.search.message-\(client.server)") message: "explore.search.message-\(client.server)")
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.secondaryBackgroundColor) .listRowBackground(theme.secondaryBackgroundColor)
#endif #endif
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
} else { } else {
quickAccessView quickAccessView
@ -94,32 +94,32 @@ public struct ExploreView: View {
} }
.listStyle(.plain) .listStyle(.plain)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
.navigationTitle("explore.navigation-title") .navigationTitle("explore.navigation-title")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.searchable(text: $viewModel.searchQuery, .searchable(text: $viewModel.searchQuery,
isPresented: $viewModel.isSearchPresented, isPresented: $viewModel.isSearchPresented,
placement: .navigationBarDrawer(displayMode: .always), placement: .navigationBarDrawer(displayMode: .always),
prompt: Text("explore.search.prompt")) prompt: Text("explore.search.prompt"))
.searchScopes($viewModel.searchScope) { .searchScopes($viewModel.searchScope) {
ForEach(ExploreViewModel.SearchScope.allCases, id: \.self) { scope in ForEach(ExploreViewModel.SearchScope.allCases, id: \.self) { scope in
Text(scope.localizedString) Text(scope.localizedString)
} }
} }
.task(id: viewModel.searchQuery) { .task(id: viewModel.searchQuery) {
await viewModel.search() await viewModel.search()
} }
.onChange(of: scrollToTopSignal) { .onChange(of: scrollToTopSignal) {
if viewModel.scrollToTopVisible { if viewModel.scrollToTopVisible {
viewModel.isSearchPresented.toggle() viewModel.isSearchPresented.toggle()
} else { } else {
withAnimation { withAnimation {
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top) proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
}
} }
} }
}
} }
} }
@ -148,9 +148,9 @@ public struct ExploreView: View {
.scrollIndicators(.never) .scrollIndicators(.never)
.listRowInsets(EdgeInsets()) .listRowInsets(EdgeInsets())
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.secondaryBackgroundColor) .listRowBackground(theme.secondaryBackgroundColor)
#endif #endif
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
} }
private var loadingView: some View { private var loadingView: some View {
@ -159,9 +159,9 @@ public struct ExploreView: View {
.padding(.vertical, 8) .padding(.vertical, 8)
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
.allowsHitTesting(false) .allowsHitTesting(false)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }
} }
@ -172,13 +172,13 @@ public struct ExploreView: View {
ForEach(results.accounts) { account in ForEach(results.accounts) { account in
if let relationship = results.relationships.first(where: { $0.id == account.id }) { if let relationship = results.relationships.first(where: { $0.id == account.id }) {
AccountsListRow(viewModel: .init(account: account, relationShip: relationship)) AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
} }
} }
} }
@ -187,13 +187,13 @@ public struct ExploreView: View {
Section("explore.section.tags") { Section("explore.section.tags") {
ForEach(results.hashtags) { tag in ForEach(results.hashtags) { tag in
TagRowView(tag: tag) TagRowView(tag: tag)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
.padding(.vertical, 4) .padding(.vertical, 4)
} }
} }
@ -202,13 +202,13 @@ public struct ExploreView: View {
Section("explore.section.posts") { Section("explore.section.posts") {
ForEach(results.statuses) { status in ForEach(results.statuses) { status in
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath)) StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
.padding(.vertical, 8) .padding(.vertical, 8)
} }
} }
@ -222,13 +222,13 @@ public struct ExploreView: View {
{ account in { account in
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) { if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
AccountsListRow(viewModel: .init(account: account, relationShip: relationship)) AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
} }
} }
NavigationLink(value: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) { NavigationLink(value: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) {
@ -239,7 +239,7 @@ public struct ExploreView: View {
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
} }
@ -251,13 +251,13 @@ public struct ExploreView: View {
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) .prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count))
{ tag in { tag in
TagRowView(tag: tag) TagRowView(tag: tag)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
.padding(.vertical, 4) .padding(.vertical, 4)
} }
NavigationLink(value: RouterDestination.tagsList(tags: viewModel.trendingTags)) { NavigationLink(value: RouterDestination.tagsList(tags: viewModel.trendingTags)) {
@ -268,7 +268,7 @@ public struct ExploreView: View {
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
} }
@ -280,13 +280,13 @@ public struct ExploreView: View {
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) .prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count))
{ status in { status in
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath)) StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
.padding(.vertical, 8) .padding(.vertical, 8)
} }
@ -298,7 +298,7 @@ public struct ExploreView: View {
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
} }
@ -311,13 +311,13 @@ public struct ExploreView: View {
{ card in { card in
StatusRowCardView(card: card) StatusRowCardView(card: card)
.environment(\.isCompact, true) .environment(\.isCompact, true)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
.padding(.vertical, 8) .padding(.vertical, 8)
} }
@ -329,7 +329,7 @@ public struct ExploreView: View {
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#else #else
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background).hoverEffect()) .foregroundStyle(.background).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#endif #endif
} }

View file

@ -15,9 +15,9 @@ public struct TagsListView: View {
List { List {
ForEach(tags) { tag in ForEach(tags) { tag in
TagRowView(tag: tag) TagRowView(tag: tag)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
.padding(.vertical, 4) .padding(.vertical, 4)
} }
} }

View file

@ -1,8 +1,8 @@
import DesignSystem import DesignSystem
import Models import Models
import Network
import StatusKit import StatusKit
import SwiftUI import SwiftUI
import Network
public struct TrendingLinksListView: View { public struct TrendingLinksListView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@ -19,9 +19,9 @@ public struct TrendingLinksListView: View {
ForEach(links) { card in ForEach(links) { card in
StatusRowCardView(card: card) StatusRowCardView(card: card)
.environment(\.isCompact, true) .environment(\.isCompact, true)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
.padding(.vertical, 8) .padding(.vertical, 8)
} }
NextPageView { NextPageView {

View file

@ -1,5 +1,5 @@
import SwiftUI
import Models import Models
import SwiftUI
enum DisplayType { enum DisplayType {
case image case image

View file

@ -1,9 +1,9 @@
import AVKit import AVKit
import DesignSystem import DesignSystem
import Env import Env
import Models
import Observation import Observation
import SwiftUI import SwiftUI
import Models
@MainActor @MainActor
@Observable public class MediaUIAttachmentVideoViewModel { @Observable public class MediaUIAttachmentVideoViewModel {
@ -21,9 +21,9 @@ import Models
player = .init(url: url) player = .init(url: url)
player?.audiovisualBackgroundPlaybackPolicy = .pauses player?.audiovisualBackgroundPlaybackPolicy = .pauses
#if !os(visionOS) #if !os(visionOS)
player?.preventsDisplaySleepDuringVideoPlayback = false player?.preventsDisplaySleepDuringVideoPlayback = false
#endif #endif
if (autoPlay || forceAutoPlay) && !isCompact { if autoPlay || forceAutoPlay, !isCompact {
player?.play() player?.play()
isPlaying = true isPlaying = true
} else { } else {
@ -41,7 +41,7 @@ import Models
} }
} }
} }
func mute(_ mute: Bool) { func mute(_ mute: Bool) {
player?.isMuted = mute player?.isMuted = mute
} }
@ -50,7 +50,7 @@ import Models
isPlaying = false isPlaying = false
player?.pause() player?.pause()
} }
func stop() { func stop() {
isPlaying = false isPlaying = false
player?.pause() player?.pause()
@ -62,15 +62,15 @@ import Models
player?.seek(to: CMTime.zero) player?.seek(to: CMTime.zero)
player?.play() player?.play()
} }
func resume() { func resume() {
isPlaying = true isPlaying = true
player?.play() player?.play()
} }
func preventSleep(_ preventSleep: Bool) { func preventSleep(_ preventSleep: Bool) {
#if !os(visionOS) #if !os(visionOS)
player?.preventsDisplaySleepDuringVideoPlayback = preventSleep player?.preventsDisplaySleepDuringVideoPlayback = preventSleep
#endif #endif
} }
@ -96,90 +96,91 @@ public struct MediaUIAttachmentVideoView: View {
public var body: some View { public var body: some View {
videoView videoView
.onAppear {
viewModel.preparePlayer(autoPlay: isFullScreen ? true : preferences.autoPlayVideo,
isCompact: isCompact)
viewModel.mute(preferences.muteVideo)
}
.onDisappear {
viewModel.stop()
}
.onTapGesture {
if !preferences.autoPlayVideo && !viewModel.isPlaying {
viewModel.play()
return
}
#if targetEnvironment(macCatalyst)
viewModel.pause()
let attachement = MediaAttachment.videoWith(url: viewModel.url)
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement], selectedAttachment: attachement))
#else
isFullScreen = true
#endif
}
.fullScreenCover(isPresented: $isFullScreen) {
NavigationStack {
videoView
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button { isFullScreen.toggle() } label: {
Image(systemName: "xmark.circle")
}
}
QuickLookToolbarItem(itemUrl: viewModel.url)
}
}
.onAppear { .onAppear {
DispatchQueue.global().async { viewModel.preparePlayer(autoPlay: isFullScreen ? true : preferences.autoPlayVideo,
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) isCompact: isCompact)
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .duckOthers) viewModel.mute(preferences.muteVideo)
try? AVAudioSession.sharedInstance().setActive(true)
}
viewModel.preventSleep(true)
viewModel.mute(false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
if isCompact || !preferences.autoPlayVideo {
viewModel.play()
} else {
viewModel.resume()
}
}
} }
.onDisappear { .onDisappear {
if isCompact || !preferences.autoPlayVideo { viewModel.stop()
viewModel.pause()
}
viewModel.preventSleep(false)
viewModel.mute(preferences.muteVideo)
DispatchQueue.global().async {
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
try? AVAudioSession.sharedInstance().setActive(true)
}
} }
} .onTapGesture {
.cornerRadius(4) if !preferences.autoPlayVideo && !viewModel.isPlaying {
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .background, .inactive:
viewModel.pause()
case .active:
if (preferences.autoPlayVideo || viewModel.forceAutoPlay || isFullScreen) && !isCompact {
viewModel.play() viewModel.play()
return
}
#if targetEnvironment(macCatalyst)
viewModel.pause()
let attachement = MediaAttachment.videoWith(url: viewModel.url)
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement], selectedAttachment: attachement))
#else
isFullScreen = true
#endif
}
.fullScreenCover(isPresented: $isFullScreen) {
NavigationStack {
videoView
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button { isFullScreen.toggle() } label: {
Image(systemName: "xmark.circle")
}
}
QuickLookToolbarItem(itemUrl: viewModel.url)
}
}
.onAppear {
DispatchQueue.global().async {
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .duckOthers)
try? AVAudioSession.sharedInstance().setActive(true)
}
viewModel.preventSleep(true)
viewModel.mute(false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
if isCompact || !preferences.autoPlayVideo {
viewModel.play()
} else {
viewModel.resume()
}
}
}
.onDisappear {
if isCompact || !preferences.autoPlayVideo {
viewModel.pause()
}
viewModel.preventSleep(false)
viewModel.mute(preferences.muteVideo)
DispatchQueue.global().async {
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
try? AVAudioSession.sharedInstance().setActive(true)
}
}
}
.cornerRadius(4)
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .background, .inactive:
viewModel.pause()
case .active:
if (preferences.autoPlayVideo || viewModel.forceAutoPlay || isFullScreen) && !isCompact {
viewModel.play()
}
default:
break
} }
default:
break
} }
}
} }
private var videoView: some View { private var videoView: some View {
VideoPlayer(player: viewModel.player, videoOverlay: { VideoPlayer(player: viewModel.player, videoOverlay: {
if !preferences.autoPlayVideo, if !preferences.autoPlayVideo,
!viewModel.forceAutoPlay, !viewModel.forceAutoPlay,
!isFullScreen, !isFullScreen,
!viewModel.isPlaying, !viewModel.isPlaying,
!isCompact { !isCompact
{
Button(action: { Button(action: {
viewModel.play() viewModel.play()
}, label: { }, label: {

View file

@ -1,8 +1,8 @@
import AVFoundation
import Models import Models
import Nuke import Nuke
import QuickLook import QuickLook
import SwiftUI import SwiftUI
import AVFoundation
public struct MediaUIView: View, @unchecked Sendable { public struct MediaUIView: View, @unchecked Sendable {
private let data: [DisplayData] private let data: [DisplayData]

View file

@ -1,6 +1,6 @@
import SwiftUI
import NukeUI
import Nuke import Nuke
import NukeUI
import SwiftUI
struct QuickLookToolbarItem: ToolbarContent, @unchecked Sendable { struct QuickLookToolbarItem: ToolbarContent, @unchecked Sendable {
let itemUrl: URL let itemUrl: URL

View file

@ -90,14 +90,14 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
self.source = source self.source = source
self.bot = bot self.bot = bot
self.discoverable = discoverable self.discoverable = discoverable
if let displayName, !displayName.isEmpty { if let displayName, !displayName.isEmpty {
self.cachedDisplayName = .init(stringValue: displayName) cachedDisplayName = .init(stringValue: displayName)
} else { } else {
self.cachedDisplayName = .init(stringValue: "@\(username)") cachedDisplayName = .init(stringValue: "@\(username)")
} }
} }
public enum CodingKeys: CodingKey { public enum CodingKeys: CodingKey {
case id case id
case username case username
@ -119,32 +119,32 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
case bot case bot
case discoverable case discoverable
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id) id = try container.decode(String.self, forKey: .id)
self.username = try container.decode(String.self, forKey: .username) username = try container.decode(String.self, forKey: .username)
self.displayName = try container.decodeIfPresent(String.self, forKey: .displayName) displayName = try container.decodeIfPresent(String.self, forKey: .displayName)
self.avatar = try container.decode(URL.self, forKey: .avatar) avatar = try container.decode(URL.self, forKey: .avatar)
self.header = try container.decode(URL.self, forKey: .header) header = try container.decode(URL.self, forKey: .header)
self.acct = try container.decode(String.self, forKey: .acct) acct = try container.decode(String.self, forKey: .acct)
self.note = try container.decode(HTMLString.self, forKey: .note) note = try container.decode(HTMLString.self, forKey: .note)
self.createdAt = try container.decode(ServerDate.self, forKey: .createdAt) createdAt = try container.decode(ServerDate.self, forKey: .createdAt)
self.followersCount = try container.decodeIfPresent(Int.self, forKey: .followersCount) followersCount = try container.decodeIfPresent(Int.self, forKey: .followersCount)
self.followingCount = try container.decodeIfPresent(Int.self, forKey: .followingCount) followingCount = try container.decodeIfPresent(Int.self, forKey: .followingCount)
self.statusesCount = try container.decodeIfPresent(Int.self, forKey: .statusesCount) statusesCount = try container.decodeIfPresent(Int.self, forKey: .statusesCount)
self.lastStatusAt = try container.decodeIfPresent(String.self, forKey: .lastStatusAt) lastStatusAt = try container.decodeIfPresent(String.self, forKey: .lastStatusAt)
self.fields = try container.decode([Account.Field].self, forKey: .fields) fields = try container.decode([Account.Field].self, forKey: .fields)
self.locked = try container.decode(Bool.self, forKey: .locked) locked = try container.decode(Bool.self, forKey: .locked)
self.emojis = try container.decode([Emoji].self, forKey: .emojis) emojis = try container.decode([Emoji].self, forKey: .emojis)
self.url = try container.decodeIfPresent(URL.self, forKey: .url) url = try container.decodeIfPresent(URL.self, forKey: .url)
self.source = try container.decodeIfPresent(Account.Source.self, forKey: .source) source = try container.decodeIfPresent(Account.Source.self, forKey: .source)
self.bot = try container.decode(Bool.self, forKey: .bot) bot = try container.decode(Bool.self, forKey: .bot)
self.discoverable = try container.decodeIfPresent(Bool.self, forKey: .discoverable) discoverable = try container.decodeIfPresent(Bool.self, forKey: .discoverable)
if let displayName, !displayName.isEmpty { if let displayName, !displayName.isEmpty {
self.cachedDisplayName = .init(stringValue: displayName) cachedDisplayName = .init(stringValue: displayName)
} else { } else {
self.cachedDisplayName = .init(stringValue: "@\(username)") cachedDisplayName = .init(stringValue: "@\(username)")
} }
} }

View file

@ -15,14 +15,14 @@ public struct ServerDate: Codable, Hashable, Equatable, Sendable {
relativeTo: Date()) relativeTo: Date())
} else { } else {
return Duration.seconds(-date.timeIntervalSinceNow).formatted(.units(width: .narrow, return Duration.seconds(-date.timeIntervalSinceNow).formatted(.units(width: .narrow,
maximumUnitCount: 1)) maximumUnitCount: 1))
} }
} }
public var shortDateFormatted: String { public var shortDateFormatted: String {
DateFormatterCache.shared.createdAtShortDateFormatted.string(from: asDate) DateFormatterCache.shared.createdAtShortDateFormatted.string(from: asDate)
} }
private static let calendar = Calendar(identifier: .gregorian) private static let calendar = Calendar(identifier: .gregorian)
public init() { public init() {
@ -41,7 +41,7 @@ public struct ServerDate: Codable, Hashable, Equatable, Sendable {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
asDate = try container.decode(Date.self, forKey: .asDate) asDate = try container.decode(Date.self, forKey: .asDate)
} }
let aDay: TimeInterval = 60 * 60 * 24 let aDay: TimeInterval = 60 * 60 * 24
isOlderThanADay = Date().timeIntervalSince(asDate) >= aDay isOlderThanADay = Date().timeIntervalSince(asDate) >= aDay
} }

View file

@ -38,8 +38,8 @@ public struct ConsolidatedNotification: Identifiable {
} }
public static func placeholders() -> [ConsolidatedNotification] { public static func placeholders() -> [ConsolidatedNotification] {
[.placeholder(), .placeholder(), .placeholder(), [.placeholder(), .placeholder(), .placeholder(),
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(),
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(),
.placeholder(), .placeholder(), .placeholder()] .placeholder(), .placeholder(), .placeholder()]
} }

View file

@ -13,7 +13,7 @@ public struct List: Codable, Identifiable, Equatable, Hashable {
case followed, list, none case followed, list, none
} }
public init(id: String, title: String, repliesPolicy: RepliesPolicy? = nil, exclusive: Bool? = nil) { public init(id: String, title: String, repliesPolicy: RepliesPolicy? = nil, exclusive: Bool? = nil) {
self.id = id self.id = id
self.title = title self.title = title

View file

@ -6,7 +6,7 @@ public struct Marker: Codable, Sendable {
public let version: Int public let version: Int
public let updatedAt: ServerDate public let updatedAt: ServerDate
} }
public let notifications: Content? public let notifications: Content?
public let home: Content? public let home: Content?
} }

View file

@ -53,7 +53,7 @@ public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable {
description: nil, description: nil,
meta: nil) meta: nil)
} }
public static func videoWith(url: URL) -> MediaAttachment { public static func videoWith(url: URL) -> MediaAttachment {
.init(id: UUID().uuidString, .init(id: UUID().uuidString,
type: "video", type: "video",

View file

@ -5,7 +5,7 @@ public struct OauthToken: Codable, Hashable, Sendable {
public let tokenType: String public let tokenType: String
public let scope: String public let scope: String
public let createdAt: Double public let createdAt: Double
public init(accessToken: String, tokenType: String, scope: String, createdAt: Double) { public init(accessToken: String, tokenType: String, scope: String, createdAt: Double) {
self.accessToken = accessToken self.accessToken = accessToken
self.tokenType = tokenType self.tokenType = tokenType

View file

@ -1,15 +1,15 @@
import Foundation import Foundation
public enum PostError: Error { public enum PostError: Error {
// Throw when any attached media is missing media description (alt text) // Throw when any attached media is missing media description (alt text)
case missingAltText case missingAltText
} }
extension PostError: CustomStringConvertible { extension PostError: CustomStringConvertible {
public var description: String { public var description: String {
switch self { switch self {
case .missingAltText: case .missingAltText:
return NSLocalizedString("status.error.no-alt-text", comment: "media does not have media description") return NSLocalizedString("status.error.no-alt-text", comment: "media does not have media description")
}
} }
}
} }

View file

@ -73,7 +73,7 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
public let filtered: [Filtered]? public let filtered: [Filtered]?
public let sensitive: Bool public let sensitive: Bool
public let language: String? public let language: String?
public var isHidden: Bool { public var isHidden: Bool {
filtered?.first?.filter.filterAction == .hide filtered?.first?.filter.filterAction == .hide
} }
@ -214,7 +214,7 @@ public final class ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Ha
public let filtered: [Filtered]? public let filtered: [Filtered]?
public let sensitive: Bool public let sensitive: Bool
public let language: String? public let language: String?
public var isHidden: Bool { public var isHidden: Bool {
filtered?.first?.filter.filterAction == .hide filtered?.first?.filter.filterAction == .hide
} }

View file

@ -22,7 +22,7 @@ public struct StreamEventUpdate: StreamEvent {
public struct StreamEventStatusUpdate: StreamEvent { public struct StreamEventStatusUpdate: StreamEvent {
public let date = Date() public let date = Date()
public var id: String { status.id + (status.editedAt?.asDate.description ?? "")} public var id: String { status.id + (status.editedAt?.asDate.description ?? "") }
public let status: Status public let status: Status
public init(status: Status) { public init(status: Status) {
self.status = status self.status = status

View file

@ -7,7 +7,7 @@ public struct Tag: Codable, Identifiable, Equatable, Hashable {
public static func == (lhs: Tag, rhs: Tag) -> Bool { public static func == (lhs: Tag, rhs: Tag) -> Bool {
lhs.name == rhs.name && lhs.name == rhs.name &&
lhs.following == rhs.following lhs.following == rhs.following
} }
public var id: String { public var id: String {

View file

@ -3,8 +3,8 @@ import Foundation
import Models import Models
import Observation import Observation
import os import os
import SwiftUI
import OSLog import OSLog
import SwiftUI
@Observable public final class Client: Equatable, Identifiable, Hashable, @unchecked Sendable { @Observable public final class Client: Equatable, Identifiable, Hashable, @unchecked Sendable {
public static func == (lhs: Client, rhs: Client) -> Bool { public static func == (lhs: Client, rhs: Client) -> Bool {
@ -44,7 +44,7 @@ import OSLog
public let version: Version public let version: Version
private let urlSession: URLSession private let urlSession: URLSession
private let decoder = JSONDecoder() private let decoder = JSONDecoder()
private let logger = Logger(subsystem: "com.icecubesapp", category: "networking") private let logger = Logger(subsystem: "com.icecubesapp", category: "networking")
// Putting all mutable state inside an `OSAllocatedUnfairLock` makes `Client` // Putting all mutable state inside an `OSAllocatedUnfairLock` makes `Client`
@ -263,7 +263,7 @@ import OSLog
throw error throw error
} }
} }
public func mediaUpload(endpoint: Endpoint, public func mediaUpload(endpoint: Endpoint,
version: Version, version: Version,
method: String, method: String,
@ -280,13 +280,14 @@ import OSLog
let (_, httpResponse) = try await urlSession.data(for: request) let (_, httpResponse) = try await urlSession.data(for: request)
return httpResponse as? HTTPURLResponse return httpResponse as? HTTPURLResponse
} }
private func makeFormDataRequest(endpoint: Endpoint, private func makeFormDataRequest(endpoint: Endpoint,
version: Version, version: Version,
method: String, method: String,
mimeType: String, mimeType: String,
filename: String, filename: String,
data: Data) throws -> URLRequest { data: Data) throws -> URLRequest
{
let url = try makeURL(endpoint: endpoint, forceVersion: version) let url = try makeURL(endpoint: endpoint, forceVersion: version)
var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method)
let boundary = UUID().uuidString let boundary = UUID().uuidString

View file

@ -98,11 +98,11 @@ public enum Accounts: Endpoint {
if let sinceId { if let sinceId {
params.append(.init(name: "max_id", value: sinceId)) params.append(.init(name: "max_id", value: sinceId))
} }
params.append(.init(name: "only_media", value: onlyMedia ? "true" : "false")) params.append(.init(name: "only_media", value: onlyMedia ? "true" : "false"))
params.append(.init(name: "exclude_replies", value: excludeReplies ? "true" : "false")) params.append(.init(name: "exclude_replies", value: excludeReplies ? "true" : "false"))
params.append(.init(name: "exclude_reblogs", value: excludeReblogs ? "true" : "false")) params.append(.init(name: "exclude_reblogs", value: excludeReblogs ? "true" : "false"))
if let pinned { if let pinned {
params.append(.init(name: "pinned", value: pinned ? "true" : "false")) params.append(.init(name: "pinned", value: pinned ? "true" : "false"))
} }

View file

@ -34,8 +34,8 @@ public struct InstanceSocialClient: Sendable {
} }
} }
extension Array where Self.Element == InstanceSocial { private extension Array where Self.Element == InstanceSocial {
fileprivate func sorted(by keyword: String) -> Self { func sorted(by keyword: String) -> Self {
let keyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines) let keyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
var newArray = self var newArray = self

View file

@ -160,7 +160,7 @@ struct NotificationRowView: View {
client: client, client: client,
routerPath: routerPath, routerPath: routerPath,
showActions: true)) showActions: true))
.environment(\.isMediaCompact, false) .environment(\.isMediaCompact, false)
} else { } else {
StatusRowView(viewModel: .init(status: status, StatusRowView(viewModel: .init(status: status,
client: client, client: client,

View file

@ -88,44 +88,44 @@ public struct NotificationsListView: View {
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor) .background(theme.primaryBackgroundColor)
#endif #endif
.onAppear { .onAppear {
viewModel.client = client viewModel.client = client
viewModel.currentAccount = account viewModel.currentAccount = account
if let lockedType { if let lockedType {
viewModel.isLockedType = true viewModel.isLockedType = true
viewModel.selectedType = lockedType viewModel.selectedType = lockedType
} else { } else {
viewModel.loadSelectedType() viewModel.loadSelectedType()
} }
Task {
await viewModel.fetchNotifications()
}
}
.refreshable {
SoundEffectManager.shared.playSound(.pull)
HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
await viewModel.fetchNotifications()
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)
}
}
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:
Task { Task {
await viewModel.fetchNotifications() await viewModel.fetchNotifications()
} }
default:
break
} }
} .refreshable {
SoundEffectManager.shared.playSound(.pull)
HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
await viewModel.fetchNotifications()
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)
}
}
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:
Task {
await viewModel.fetchNotifications()
}
default:
break
}
}
} }
@ViewBuilder @ViewBuilder
@ -141,14 +141,14 @@ public struct NotificationsListView: View {
leading: .layoutPadding + 4, leading: .layoutPadding + 4,
bottom: 0, bottom: 0,
trailing: .layoutPadding)) trailing: .layoutPadding))
#if os(visionOS) #if os(visionOS)
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background)) .foregroundStyle(.background))
#else #else
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
.allowsHitTesting(false) .allowsHitTesting(false)
} }
case let .display(notifications, nextPageState): case let .display(notifications, nextPageState):
@ -156,9 +156,9 @@ public struct NotificationsListView: View {
EmptyView(iconName: "bell.slash", EmptyView(iconName: "bell.slash",
title: "notifications.empty.title", title: "notifications.empty.title",
message: "notifications.empty.message") message: "notifications.empty.message")
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
.listSectionSeparator(.hidden) .listSectionSeparator(.hidden)
} else { } else {
ForEach(notifications) { notification in ForEach(notifications) { notification in
@ -170,17 +170,17 @@ public struct NotificationsListView: View {
leading: .layoutPadding + 4, leading: .layoutPadding + 4,
bottom: 6, bottom: 6,
trailing: .layoutPadding)) trailing: .layoutPadding))
#if os(visionOS) #if os(visionOS)
.listRowBackground(RoundedRectangle(cornerRadius: 8) .listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(notification.type == .mention && lockedType != .mention ? Material.thick : Material.regular).hoverEffect()) .foregroundStyle(notification.type == .mention && lockedType != .mention ? Material.thick : Material.regular).hoverEffect())
.listRowHoverEffectDisabled() .listRowHoverEffectDisabled()
#else #else
.listRowBackground(notification.type == .mention && lockedType != .mention ? .listRowBackground(notification.type == .mention && lockedType != .mention ?
theme.secondaryBackgroundColor : theme.primaryBackgroundColor) theme.secondaryBackgroundColor : theme.primaryBackgroundColor)
#endif #endif
.id(notification.id) .id(notification.id)
} }
switch nextPageState { switch nextPageState {
case .none: case .none:
EmptyView() EmptyView()
@ -193,7 +193,7 @@ public struct NotificationsListView: View {
bottom: .layoutPadding, bottom: .layoutPadding,
trailing: .layoutPadding)) trailing: .layoutPadding))
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }
} }

View file

@ -96,7 +96,7 @@ import SwiftUI
newNotifications = newNotifications.filter { notification in newNotifications = newNotifications.filter { notification in
!consolidatedNotifications.contains(where: { $0.id == notification.id }) !consolidatedNotifications.contains(where: { $0.id == notification.id })
} }
await consolidatedNotifications.insert( await consolidatedNotifications.insert(
contentsOf: newNotifications.consolidated(selectedType: selectedType), contentsOf: newNotifications.consolidated(selectedType: selectedType),
at: 0 at: 0
@ -108,7 +108,7 @@ import SwiftUI
} }
markAsRead() markAsRead()
withAnimation { withAnimation {
state = .display(notifications: consolidatedNotifications, state = .display(notifications: consolidatedNotifications,
nextPageState: consolidatedNotifications.isEmpty ? .none : nextPageState) nextPageState: consolidatedNotifications.isEmpty ? .none : nextPageState)
@ -158,13 +158,13 @@ import SwiftUI
state = .display(notifications: consolidatedNotifications, state = .display(notifications: consolidatedNotifications,
nextPageState: newNotifications.count < Constants.notificationLimit ? .none : .hasNextPage) nextPageState: newNotifications.count < Constants.notificationLimit ? .none : .hasNextPage)
} }
func markAsRead() { func markAsRead() {
guard let client, let id = consolidatedNotifications.first?.notifications.first?.id else { return } guard let client, let id = consolidatedNotifications.first?.notifications.first?.id else { return }
Task { Task {
do { do {
let _: Marker = try await client.post(endpoint: Markers.markNotifications(lastReadId: id)) let _: Marker = try await client.post(endpoint: Markers.markNotifications(lastReadId: id))
} catch { } } catch {}
} }
} }

View file

@ -54,15 +54,15 @@ public struct StatusDetailView: View {
loadingContextView loadingContextView
} }
#if !os(visionOS) #if !os(visionOS)
Rectangle() Rectangle()
.foregroundColor(theme.secondaryBackgroundColor) .foregroundColor(theme.secondaryBackgroundColor)
.frame(minHeight: reader.frame(in: .local).size.height - statusHeight) .frame(minHeight: reader.frame(in: .local).size.height - statusHeight)
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.listRowBackground(theme.secondaryBackgroundColor) .listRowBackground(theme.secondaryBackgroundColor)
.listRowInsets(.init()) .listRowInsets(.init())
.accessibilityHidden(true) .accessibilityHidden(true)
#endif #endif
case .error: case .error:
errorView errorView
@ -71,33 +71,33 @@ public struct StatusDetailView: View {
.environment(\.defaultMinListRowHeight, 1) .environment(\.defaultMinListRowHeight, 1)
.listStyle(.plain) .listStyle(.plain)
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor) .background(theme.primaryBackgroundColor)
#endif #endif
.onChange(of: viewModel.scrollToId) { _, newValue in .onChange(of: viewModel.scrollToId) { _, newValue in
if let newValue { if let newValue {
viewModel.scrollToId = nil viewModel.scrollToId = nil
proxy.scrollTo(newValue, anchor: .top) proxy.scrollTo(newValue, anchor: .top)
}
} }
} .onAppear {
.onAppear { guard !isLoaded else { return }
guard !isLoaded else { return } viewModel.client = client
viewModel.client = client viewModel.routerPath = routerPath
viewModel.routerPath = routerPath Task {
Task { let result = await viewModel.fetch()
let result = await viewModel.fetch() isLoaded = true
isLoaded = true
if !result { if !result {
if let url = viewModel.remoteStatusURL { if let url = viewModel.remoteStatusURL {
await UIApplication.shared.open(url) await UIApplication.shared.open(url)
} }
DispatchQueue.main.async { DispatchQueue.main.async {
_ = routerPath.path.popLast() _ = routerPath.path.popLast()
}
} }
} }
} }
}
} }
.refreshable { .refreshable {
Task { Task {
@ -137,9 +137,9 @@ public struct StatusDetailView: View {
} }
} }
} }
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(viewModel.highlightRowColor) .listRowBackground(viewModel.highlightRowColor)
#endif #endif
.listRowInsets(.init(top: 12, .listRowInsets(.init(top: 12,
leading: .layoutPadding, leading: .layoutPadding,
bottom: 12, bottom: 12,
@ -179,16 +179,16 @@ public struct StatusDetailView: View {
.frame(height: 50) .frame(height: 50)
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.secondaryBackgroundColor) .listRowBackground(theme.secondaryBackgroundColor)
#endif #endif
.listRowInsets(.init()) .listRowInsets(.init())
} }
private var topPaddingView: some View { private var topPaddingView: some View {
HStack { EmptyView() } HStack { EmptyView() }
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.listRowInsets(.init()) .listRowInsets(.init())
.frame(height: .layoutPadding) .frame(height: .layoutPadding)

View file

@ -37,5 +37,4 @@ extension StatusEditor {
} }
} }
} }
} }

View file

@ -1,7 +1,7 @@
import DesignSystem import DesignSystem
import Env import Env
#if !os(visionOS) && !DEBUG #if !os(visionOS) && !DEBUG
import GiphyUISDK import GiphyUISDK
#endif #endif
import Models import Models
import NukeUI import NukeUI
@ -29,47 +29,47 @@ extension StatusEditor {
var body: some View { var body: some View {
@Bindable var viewModel = focusedSEVM @Bindable var viewModel = focusedSEVM
#if os(visionOS) #if os(visionOS)
HStack { HStack {
contentView contentView
.buttonStyle(.borderless) .buttonStyle(.borderless)
} }
.frame(width: 32) .frame(width: 32)
.padding(16) .padding(16)
.glassBackgroundEffect() .glassBackgroundEffect()
.cornerRadius(8) .cornerRadius(8)
.padding(.trailing, 78) .padding(.trailing, 78)
#else #else
Divider() Divider()
HStack { HStack {
contentView contentView
} }
.frame(height: 20) .frame(height: 20)
.padding(.vertical, 12) .padding(.vertical, 12)
.background(.ultraThickMaterial) .background(.ultraThickMaterial)
#endif #endif
} }
@ViewBuilder @ViewBuilder
private var contentView: some View { private var contentView: some View {
#if os(visionOS) #if os(visionOS)
VStack(spacing: 8) { VStack(spacing: 8) {
actionsView
}
#else
ViewThatFits {
HStack(alignment: .center, spacing: 16) {
actionsView actionsView
} }
.padding(.horizontal, .layoutPadding) #else
ViewThatFits {
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 16) { HStack(alignment: .center, spacing: 16) {
actionsView actionsView
} }
.padding(.horizontal, .layoutPadding) .padding(.horizontal, .layoutPadding)
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 16) {
actionsView
}
.padding(.horizontal, .layoutPadding)
}
.scrollIndicators(.hidden)
} }
.scrollIndicators(.hidden)
}
#endif #endif
} }
@ -96,11 +96,11 @@ extension StatusEditor {
} }
#if !os(visionOS) #if !os(visionOS)
Button { Button {
isGIFPickerPresented = true isGIFPickerPresented = true
} label: { } label: {
Label("GIPHY", systemImage: "party.popper") Label("GIPHY", systemImage: "party.popper")
} }
#endif #endif
} label: { } label: {
if viewModel.isMediasLoading { if viewModel.isMediasLoading {
@ -135,31 +135,30 @@ extension StatusEditor {
.sheet(isPresented: $isGIFPickerPresented, content: { .sheet(isPresented: $isGIFPickerPresented, content: {
#if !os(visionOS) && !DEBUG #if !os(visionOS) && !DEBUG
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
NavigationStack { NavigationStack {
giphyView giphyView
.toolbar { .toolbar {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Button { Button {
isGIFPickerPresented = false isGIFPickerPresented = false
} label: { } label: {
Image(systemName: "xmark.circle") Image(systemName: "xmark.circle")
}
}
} }
}
} }
}
.presentationDetents([.medium, .large])
#else
giphyView
.presentationDetents([.medium, .large]) .presentationDetents([.medium, .large])
#else
giphyView
.presentationDetents([.medium, .large])
#endif #endif
#else #else
EmptyView() EmptyView()
#endif #endif
}) })
.accessibilityLabel("accessibility.editor.button.attach-photo") .accessibilityLabel("accessibility.editor.button.attach-photo")
.disabled(viewModel.showPoll) .disabled(viewModel.showPoll)
Button { Button {
// all SEVM have the same visibility value // all SEVM have the same visibility value
followUpSEVMs.append(ViewModel(mode: .new(visibility: focusedSEVM.visibility))) followUpSEVMs.append(ViewModel(mode: .new(visibility: focusedSEVM.visibility)))
@ -167,7 +166,7 @@ extension StatusEditor {
Image(systemName: "arrowshape.turn.up.left.circle.fill") Image(systemName: "arrowshape.turn.up.left.circle.fill")
} }
.disabled(!canAddNewSEVM) .disabled(!canAddNewSEVM)
if !viewModel.customEmojiContainer.isEmpty { if !viewModel.customEmojiContainer.isEmpty {
Button { Button {
isCustomEmojisSheetDisplay = true isCustomEmojisSheetDisplay = true
@ -187,20 +186,19 @@ extension StatusEditor {
} }
} }
} }
if preferences.isOpenAIEnabled { if preferences.isOpenAIEnabled {
AIMenu.disabled(!viewModel.canPost) AIMenu.disabled(!viewModel.canPost)
} }
Spacer() Spacer()
Button { Button {
viewModel.insertStatusText(text: "@") viewModel.insertStatusText(text: "@")
} label: { } label: {
Image(systemName: "at") Image(systemName: "at")
} }
Button { Button {
viewModel.insertStatusText(text: "#") viewModel.insertStatusText(text: "#")
} label: { } label: {
@ -223,19 +221,19 @@ extension StatusEditor {
} }
#if !os(visionOS) && !DEBUG #if !os(visionOS) && !DEBUG
@ViewBuilder @ViewBuilder
private var giphyView: some View { private var giphyView: some View {
@Bindable var viewModel = focusedSEVM @Bindable var viewModel = focusedSEVM
GifPickerView { url in GifPickerView { url in
GPHCache.shared.downloadAssetData(url) { data, _ in GPHCache.shared.downloadAssetData(url) { data, _ in
guard let data else { return } guard let data else { return }
viewModel.processGIFData(data: data) viewModel.processGIFData(data: data)
}
isGIFPickerPresented = false
} onShouldDismissGifPicker: {
isGIFPickerPresented = false
} }
isGIFPickerPresented = false
} onShouldDismissGifPicker: {
isGIFPickerPresented = false
} }
}
#endif #endif
private var AIMenu: some View { private var AIMenu: some View {
@ -268,7 +266,5 @@ extension StatusEditor {
} }
} }
} }
} }
} }

View file

@ -1,28 +1,28 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Foundation import Foundation
import SwiftUI
import Models import Models
import SwiftData import SwiftData
import SwiftUI
extension StatusEditor { extension StatusEditor {
@MainActor @MainActor
struct AutoCompleteView: View { struct AutoCompleteView: View {
@Environment(\.modelContext) var context @Environment(\.modelContext) var context
@Environment(Theme.self) var theme @Environment(Theme.self) var theme
var viewModel: ViewModel var viewModel: ViewModel
@State private var isTagSuggestionExpanded: Bool = false @State private var isTagSuggestionExpanded: Bool = false
@Query(sort: \RecentTag.lastUse, order: .reverse) var recentTags: [RecentTag] @Query(sort: \RecentTag.lastUse, order: .reverse) var recentTags: [RecentTag]
var body: some View { var body: some View {
if !viewModel.mentionsSuggestions.isEmpty || if !viewModel.mentionsSuggestions.isEmpty ||
!viewModel.tagsSuggestions.isEmpty || !viewModel.tagsSuggestions.isEmpty ||
(viewModel.showRecentsTagsInline && !recentTags.isEmpty) { (viewModel.showRecentsTagsInline && !recentTags.isEmpty)
{
VStack { VStack {
HStack { HStack {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {

View file

@ -1,10 +1,10 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Env
import Foundation import Foundation
import SwiftUI
import Models import Models
import SwiftData import SwiftData
import Env import SwiftUI
extension StatusEditor.AutoCompleteView { extension StatusEditor.AutoCompleteView {
@MainActor @MainActor
@ -12,12 +12,12 @@ extension StatusEditor.AutoCompleteView {
@Environment(\.modelContext) private var context @Environment(\.modelContext) private var context
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
var viewModel: StatusEditor.ViewModel var viewModel: StatusEditor.ViewModel
@Binding var isTagSuggestionExpanded: Bool @Binding var isTagSuggestionExpanded: Bool
@Query(sort: \RecentTag.lastUse, order: .reverse) var recentTags: [RecentTag] @Query(sort: \RecentTag.lastUse, order: .reverse) var recentTags: [RecentTag]
var body: some View { var body: some View {
TabView { TabView {
recentTagsPage recentTagsPage
@ -26,7 +26,7 @@ extension StatusEditor.AutoCompleteView {
.tabViewStyle(.page(indexDisplayMode: .always)) .tabViewStyle(.page(indexDisplayMode: .always))
.frame(height: 200) .frame(height: 200)
} }
private var recentTagsPage: some View { private var recentTagsPage: some View {
ScrollView(.vertical) { ScrollView(.vertical) {
LazyVStack(alignment: .leading, spacing: 12) { LazyVStack(alignment: .leading, spacing: 12) {
@ -60,7 +60,7 @@ extension StatusEditor.AutoCompleteView {
.padding(.horizontal, .layoutPadding) .padding(.horizontal, .layoutPadding)
} }
} }
private var followedTagsPage: some View { private var followedTagsPage: some View {
ScrollView(.vertical) { ScrollView(.vertical) {
LazyVStack(alignment: .leading, spacing: 12) { LazyVStack(alignment: .leading, spacing: 12) {

View file

@ -1,17 +1,16 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Foundation import Foundation
import SwiftUI
import Models import Models
import SwiftData import SwiftData
import SwiftUI
extension StatusEditor.AutoCompleteView {
extension StatusEditor.AutoCompleteView {
struct MentionsView: View { struct MentionsView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
var viewModel: StatusEditor.ViewModel var viewModel: StatusEditor.ViewModel
var body: some View { var body: some View {
ForEach(viewModel.mentionsSuggestions) { account in ForEach(viewModel.mentionsSuggestions) { account in
Button { Button {

View file

@ -1,20 +1,19 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Foundation import Foundation
import SwiftUI
import Models import Models
import SwiftData import SwiftData
import SwiftUI
extension StatusEditor.AutoCompleteView {
extension StatusEditor.AutoCompleteView {
struct RecentTagsView: View { struct RecentTagsView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
var viewModel: StatusEditor.ViewModel var viewModel: StatusEditor.ViewModel
@Binding var isTagSuggestionExpanded: Bool @Binding var isTagSuggestionExpanded: Bool
@Query(sort: \RecentTag.lastUse, order: .reverse) var recentTags: [RecentTag] @Query(sort: \RecentTag.lastUse, order: .reverse) var recentTags: [RecentTag]
var body: some View { var body: some View {
ForEach(recentTags) { tag in ForEach(recentTags) { tag in
Button { Button {

View file

@ -1,21 +1,20 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Foundation import Foundation
import SwiftUI
import Models import Models
import SwiftData import SwiftData
import SwiftUI
extension StatusEditor.AutoCompleteView { extension StatusEditor.AutoCompleteView {
struct RemoteTagsView: View { struct RemoteTagsView: View {
@Environment(\.modelContext) private var context @Environment(\.modelContext) private var context
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
var viewModel: StatusEditor.ViewModel var viewModel: StatusEditor.ViewModel
@Binding var isTagSuggestionExpanded: Bool @Binding var isTagSuggestionExpanded: Bool
@Query(sort: \RecentTag.lastUse, order: .reverse) var recentTags: [RecentTag] @Query(sort: \RecentTag.lastUse, order: .reverse) var recentTags: [RecentTag]
var body: some View { var body: some View {
ForEach(viewModel.tagsSuggestions) { tag in ForEach(viewModel.tagsSuggestions) { tag in
Button { Button {

View file

@ -23,7 +23,7 @@ extension StatusEditor {
func makeUIViewController(context: Context) -> UIImagePickerController { func makeUIViewController(context: Context) -> UIImagePickerController {
let imagePicker = UIImagePickerController() let imagePicker = UIImagePickerController()
#if !os(visionOS) #if !os(visionOS)
imagePicker.sourceType = .camera imagePicker.sourceType = .camera
#endif #endif
imagePicker.delegate = context.coordinator imagePicker.delegate = context.coordinator
return imagePicker return imagePicker
@ -35,5 +35,4 @@ extension StatusEditor {
Coordinator(picker: self) Coordinator(picker: self)
} }
} }
} }

View file

@ -7,5 +7,4 @@ extension StatusEditor {
let categoryName: String let categoryName: String
var emojis: [Emoji] var emojis: [Emoji]
} }
} }

Some files were not shown because too many files have changed in this diff Show more