Merge remote-tracking branch 'upstream/main' into zh-Hant-localization

This commit is contained in:
sh95014 2024-03-23 11:14:58 -07:00
commit 45878a4d91
162 changed files with 4918 additions and 2275 deletions

View file

@ -940,7 +940,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.10.32; MARKETING_VERSION = 1.10.33;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -975,7 +975,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.10.32; MARKETING_VERSION = 1.10.33;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1011,7 +1011,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.10.32; MARKETING_VERSION = 1.10.33;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1045,7 +1045,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.10.32; MARKETING_VERSION = 1.10.33;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1225,7 +1225,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.32; MARKETING_VERSION = 1.10.33;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
PRODUCT_NAME = "Ice Cubes"; PRODUCT_NAME = "Ice Cubes";
SDKROOT = auto; SDKROOT = auto;
@ -1279,7 +1279,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.32; MARKETING_VERSION = 1.10.33;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
PRODUCT_NAME = "Ice Cubes"; PRODUCT_NAME = "Ice Cubes";
SDKROOT = auto; SDKROOT = auto;
@ -1314,7 +1314,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.10.32; MARKETING_VERSION = 1.10.33;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1349,7 +1349,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.10.32; MARKETING_VERSION = 1.10.33;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;

View file

@ -68,8 +68,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke", "location" : "https://github.com/kean/Nuke",
"state" : { "state" : {
"revision" : "15fde63470d782c897816a74bdd516a907e33147", "revision" : "8ecbfc886da39bccb01c34abef5f2ff4073ad633",
"version" : "12.3.0" "version" : "12.4.0"
} }
}, },
{ {

View file

@ -140,6 +140,12 @@ extension View {
.presentationDetents([.medium]) .presentationDetents([.medium])
.presentationBackground(.thinMaterial) .presentationBackground(.thinMaterial)
.withEnvironments() .withEnvironments()
case .accountEditInfo:
EditAccountView()
.withEnvironments()
case .accountFiltersList:
FiltersListView()
.withEnvironments()
} }
} }
} }

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

@ -17,9 +17,8 @@ struct AccountSettingsView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(AppAccountsManager.self) private var appAccountsManager @Environment(AppAccountsManager.self) private var appAccountsManager
@Environment(Client.self) private var client @Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@State private var isEditingAccount: Bool = false
@State private var isEditingFilters: Bool = false
@State private var cachedPostsCount: Int = 0 @State private var cachedPostsCount: Int = 0
@State private var timelineCache = TimelineCache() @State private var timelineCache = TimelineCache()
@ -30,7 +29,7 @@ struct AccountSettingsView: View {
Form { Form {
Section { Section {
Button { Button {
isEditingAccount = true routerPath.presentedSheet = .accountFiltersList
} label: { } label: {
Label("account.action.edit-info", systemImage: "pencil") Label("account.action.edit-info", systemImage: "pencil")
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@ -40,7 +39,7 @@ struct AccountSettingsView: View {
if currentInstance.isFiltersSupported { if currentInstance.isFiltersSupported {
Button { Button {
isEditingFilters = true routerPath.presentedSheet = .accountFiltersList
} label: { } label: {
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle") Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@ -96,12 +95,6 @@ struct AccountSettingsView: View {
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
.sheet(isPresented: $isEditingAccount, content: {
EditAccountView()
})
.sheet(isPresented: $isEditingFilters, content: {
FiltersListView()
})
.toolbar { .toolbar {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
HStack { HStack {
@ -116,8 +109,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

@ -7,6 +7,7 @@ import Observation
import StatusKit import StatusKit
import SwiftUI import SwiftUI
@MainActor
@Observable class DisplaySettingsLocalValues { @Observable class DisplaySettingsLocalValues {
var tintColor = Theme.shared.tintColor var tintColor = Theme.shared.tintColor
var primaryBackgroundColor = Theme.shared.primaryBackgroundColor var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
@ -36,11 +37,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 +50,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

@ -2,10 +2,11 @@ import DesignSystem
import Env import Env
import SwiftUI import SwiftUI
@MainActor
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 +29,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()
} }
} }
@ -158,13 +157,14 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
} }
} }
@MainActor
@Observable @Observable
class SidebarTabs { class SidebarTabs {
struct SidedebarTab: Hashable, Codable { struct SidedebarTab: Hashable, Codable {
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,36 +179,37 @@ 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
} }
} }
@MainActor
@Observable @Observable
class iOSTabs { 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 +217,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)
} }

File diff suppressed because it is too large Load diff

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

@ -207,6 +207,7 @@ struct AccountDetailHeaderView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.textSelection(.enabled) .textSelection(.enabled)
.accessibilityRespondsToUserInteraction(false) .accessibilityRespondsToUserInteraction(false)
movedToView
joinedAtView joinedAtView
} }
.accessibilityElement(children: .contain) .accessibilityElement(children: .contain)
@ -311,6 +312,17 @@ struct AccountDetailHeaderView: View {
} }
} }
@ViewBuilder
private var movedToView: some View {
if let movedTo = viewModel.account?.moved {
Button("account.movedto.redirect-\("@\(movedTo.acct)")") {
routerPath.navigate(to: .accountDetailWithAccount(account: movedTo))
}
.font(.scaledCallout)
.foregroundColor(.accentColor)
}
}
@ViewBuilder @ViewBuilder
private func makeNoteView(_ note: String) -> some View { private func makeNoteView(_ note: String) -> some View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
@ -372,15 +384,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

@ -23,11 +23,8 @@ public struct AccountDetailView: View {
@State private var viewModel: AccountDetailViewModel @State private var viewModel: AccountDetailViewModel
@State private var isCurrentUser: Bool = false @State private var isCurrentUser: Bool = false
@State private var showBlockConfirmation: Bool = false @State private var showBlockConfirmation: Bool = false
@State private var isEditingAccount: 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 +85,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 }
@ -136,20 +133,14 @@ public struct AccountDetailView: View {
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount) viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
} }
} }
.onChange(of: isEditingAccount) { _, newValue in .onChange(of: routerPath.presentedSheet) { oldValue, newValue in
if !newValue { if oldValue == .accountEditInfo || newValue == .accountEditInfo {
Task { Task {
await viewModel.fetchAccount() await viewModel.fetchAccount()
await preferences.refreshServerPreferences() await preferences.refreshServerPreferences()
} }
} }
} }
.sheet(isPresented: $isEditingAccount, content: {
EditAccountView()
})
.sheet(isPresented: $isEditingFilters, content: {
FiltersListView()
})
.sheet(isPresented: $isEditingRelationshipNote, content: { .sheet(isPresented: $isEditingRelationshipNote, content: {
EditRelationshipNoteView(accountDetailViewModel: viewModel) EditRelationshipNoteView(accountDetailViewModel: viewModel)
}) })
@ -220,7 +211,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 +237,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 +278,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")
@ -308,7 +297,7 @@ public struct AccountDetailView: View {
if isCurrentUser { if isCurrentUser {
Button { Button {
isEditingAccount = true routerPath.presentedSheet = .accountEditInfo
} label: { } label: {
Label("account.action.edit-info", systemImage: "pencil") Label("account.action.edit-info", systemImage: "pencil")
} }
@ -323,7 +312,7 @@ public struct AccountDetailView: View {
if currentInstance.isFiltersSupported { if currentInstance.isFiltersSupported {
Button { Button {
isEditingFilters = true routerPath.presentedSheet = .accountFiltersList
} label: { } label: {
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle") Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
} }
@ -370,7 +359,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 {}
} }
} }
} }
@ -382,12 +371,13 @@ public struct AccountDetailView: View {
} }
extension View { extension View {
@MainActor
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,26 @@ 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
} }
} }
} }
#Preview {
List {
AccountsListRow(viewModel: .init(account: .placeholder(),
relationShip: .placeholder()))
}
.listStyle(.plain)
.withPreviewsEnv()
.environment(Theme.shared)
}

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

@ -33,11 +33,11 @@ public struct AppAccountsSelectorView: View {
public init(routerPath: RouterPath, public init(routerPath: RouterPath,
accountCreationEnabled: Bool = true, accountCreationEnabled: Bool = true,
avatarConfig: AvatarView.FrameConfig = .badge) avatarConfig: AvatarView.FrameConfig? = nil)
{ {
self.routerPath = routerPath self.routerPath = routerPath
self.accountCreationEnabled = accountCreationEnabled self.accountCreationEnabled = accountCreationEnabled
self.avatarConfig = avatarConfig self.avatarConfig = avatarConfig ?? .badge
} }
public var body: some View { public var body: some View {
@ -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

@ -48,9 +48,9 @@ public struct ConversationsListView: View {
Divider() Divider()
} }
} else if conversations.isEmpty, !viewModel.isLoadingFirstPage, !viewModel.isError { } else if conversations.isEmpty, !viewModel.isLoadingFirstPage, !viewModel.isError {
EmptyView(iconName: "tray", PlaceholderView(iconName: "tray",
title: "conversations.empty.title", title: "conversations.empty.title",
message: "conversations.empty.message") message: "conversations.empty.message")
} else if viewModel.isError { } else if viewModel.isError {
ErrorView(title: "conversations.error.title", ErrorView(title: "conversations.error.title",
message: "conversations.error.message", message: "conversations.error.message",

View file

@ -19,7 +19,7 @@ let package = Package(
dependencies: [ dependencies: [
.package(name: "Models", path: "../Models"), .package(name: "Models", path: "../Models"),
.package(name: "Env", path: "../Env"), .package(name: "Env", path: "../Env"),
.package(url: "https://github.com/kean/Nuke", from: "12.0.0"), .package(url: "https://github.com/kean/Nuke", from: "12.4.0"),
.package(url: "https://github.com/divadretlaw/EmojiText", from: "4.0.0"), .package(url: "https://github.com/divadretlaw/EmojiText", from: "4.0.0"),
], ],
targets: [ targets: [

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

@ -2,14 +2,14 @@ import Combine
import UIKit import UIKit
@Observable @Observable
public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable { @MainActor 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
@ -47,30 +47,29 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
} }
private static var observedSceneDelegate: Set<SceneDelegate> = [] private static var observedSceneDelegate: Set<SceneDelegate> = []
private static let observer = Task { private static let observer = Task { @MainActor in
while true { while true {
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

@ -1,8 +1,10 @@
import Combine import Combine
import SwiftUI import SwiftUI
@Observable public class Theme { @MainActor
class ThemeStorage { @Observable
public final class Theme {
final class ThemeStorage {
enum ThemeKey: String { enum ThemeKey: String {
case colorScheme, tint, label, primaryBackground, secondaryBackground case colorScheme, tint, label, primaryBackground, secondaryBackground
case avatarPosition2, avatarShape2, statusActionsDisplay, statusDisplayStyle case avatarPosition2, avatarShape2, statusActionsDisplay, statusDisplayStyle
@ -69,10 +71,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:
@ -167,12 +169,14 @@ import SwiftUI
public var tintColor: Color { public var tintColor: Color {
didSet { didSet {
themeStorage.tintColor = tintColor themeStorage.tintColor = tintColor
computeContrastingTintColor()
} }
} }
public var primaryBackgroundColor: Color { public var primaryBackgroundColor: Color {
didSet { didSet {
themeStorage.primaryBackgroundColor = primaryBackgroundColor themeStorage.primaryBackgroundColor = primaryBackgroundColor
computeContrastingTintColor()
} }
} }
@ -185,6 +189,31 @@ import SwiftUI
public var labelColor: Color { public var labelColor: Color {
didSet { didSet {
themeStorage.labelColor = labelColor themeStorage.labelColor = labelColor
computeContrastingTintColor()
}
}
public private(set) var contrastingTintColor: Color
// set contrastingTintColor to either labelColor or primaryBackgroundColor, whichever contrasts
// better against the tintColor
private func computeContrastingTintColor() {
func luminance(_ color: Color.Resolved) -> Float {
return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue
}
let resolvedTintColor = tintColor.resolve(in: .init())
let resolvedLabelColor = labelColor.resolve(in: .init())
let resolvedPrimaryBackgroundColor = primaryBackgroundColor.resolve(in: .init())
let tintLuminance = luminance(resolvedTintColor)
let labelLuminance = luminance(resolvedLabelColor)
let primaryBackgroundLuminance = luminance(resolvedPrimaryBackgroundColor)
if abs(tintLuminance - labelLuminance) > abs(tintLuminance - primaryBackgroundLuminance) {
contrastingTintColor = labelColor
} else {
contrastingTintColor = primaryBackgroundColor
} }
} }
@ -217,7 +246,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
@ -273,7 +302,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
@ -281,6 +310,7 @@ import SwiftUI
primaryBackgroundColor = themeStorage.primaryBackgroundColor primaryBackgroundColor = themeStorage.primaryBackgroundColor
secondaryBackgroundColor = themeStorage.secondaryBackgroundColor secondaryBackgroundColor = themeStorage.secondaryBackgroundColor
labelColor = themeStorage.labelColor labelColor = themeStorage.labelColor
contrastingTintColor = .red // real work done in computeContrastingTintColor()
avatarPosition = themeStorage.avatarPosition avatarPosition = themeStorage.avatarPosition
avatarShape = themeStorage.avatarShape avatarShape = themeStorage.avatarShape
storedSet = themeStorage.storedSet storedSet = themeStorage.storedSet
@ -293,6 +323,8 @@ import SwiftUI
chosenFontData = themeStorage.chosenFontData chosenFontData = themeStorage.chosenFontData
statusActionSecondary = themeStorage.statusActionSecondary statusActionSecondary = themeStorage.statusActionSecondary
selectedSet = storedSet selectedSet = storedSet
computeContrastingTintColor()
} }
public static var allColorSet: [ColorSet] { public static var allColorSet: [ColorSet] {
@ -310,7 +342,7 @@ import SwiftUI
ConstellationLight(), ConstellationLight(),
ConstellationDark(), ConstellationDark(),
ThreadsLight(), ThreadsLight(),
ThreadsDark() ThreadsDark(),
] ]
} }

View file

@ -4,7 +4,7 @@ import SwiftUI
#endif #endif
public extension View { public extension View {
func applyTheme(_ theme: Theme) -> some View { @MainActor func applyTheme(_ theme: Theme) -> some View {
modifier(ThemeApplier(theme: theme)) modifier(ThemeApplier(theme: theme))
} }
} }
@ -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,6 +4,7 @@ import Nuke
import NukeUI import NukeUI
import SwiftUI import SwiftUI
@MainActor
struct AccountPopoverView: View { struct AccountPopoverView: View {
let account: Account let account: Account
let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview

View file

@ -33,7 +33,8 @@ public struct AvatarView: View {
self.config = config self.config = config
} }
public struct FrameConfig: Equatable { @MainActor
public struct FrameConfig: Equatable, Sendable {
public let size: CGSize public let size: CGSize
public var width: CGFloat { size.width } public var width: CGFloat { size.width }
public var height: CGFloat { size.height } public var height: CGFloat { size.height }

View file

@ -1,32 +0,0 @@
import SwiftUI
public struct EmptyView: View {
public let iconName: String
public let title: LocalizedStringKey
public let message: LocalizedStringKey
public init(iconName: String, title: LocalizedStringKey, message: LocalizedStringKey) {
self.iconName = iconName
self.title = title
self.message = message
}
public var body: some View {
VStack {
Image(systemName: iconName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxHeight: 50)
Text(title)
.font(.scaledTitle)
.padding(.top, 16)
Text(message)
.font(.scaledSubheadline)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
}
.padding(.top, 100)
.padding(.layoutPadding)
.fixedSize(horizontal: false, vertical: true)
}
}

View file

@ -42,3 +42,9 @@ public struct ErrorView: View {
} }
} }
} }
#Preview {
ErrorView(title: "Error",
message: "Error loading. Please try again",
buttonTitle: "Retry") {}
}

View file

@ -1,15 +1,16 @@
import SwiftUI import SwiftUI
@MainActor
public struct NextPageView: View { 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 +36,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 {
@ -50,3 +51,11 @@ public struct NextPageView: View {
} }
} }
} }
#Preview {
List {
Text("Item 1")
NextPageView {}
}
.listStyle(.plain)
}

View file

@ -0,0 +1,25 @@
import SwiftUI
public struct PlaceholderView: View {
public let iconName: String
public let title: LocalizedStringKey
public let message: LocalizedStringKey
public init(iconName: String, title: LocalizedStringKey, message: LocalizedStringKey) {
self.iconName = iconName
self.title = title
self.message = message
}
public var body: some View {
ContentUnavailableView(title,
systemImage: iconName,
description: Text(message))
}
}
#Preview {
PlaceholderView(iconName: "square.and.arrow.up.trianglebadge.exclamationmark",
title: "Nothing to see",
message: "This is a preview. Please try again.")
}

View file

@ -11,7 +11,7 @@ public struct ScrollToView: View {
public init() {} public init() {}
public var body: some View { public var body: some View {
HStack { SwiftUI.EmptyView() } HStack { EmptyView() }
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.listRowInsets(.init()) .listRowInsets(.init())

View file

@ -1,44 +0,0 @@
/*! @copyright 2021 Medium */
import SwiftUI
// Source: https://www.fivestars.blog/articles/scrollview-offset/
public struct ScrollViewOffsetReader<Content: View>: View {
let onOffsetChange: (CGFloat) -> Void
let content: () -> Content
public init(
onOffsetChange: @escaping (CGFloat) -> Void,
@ViewBuilder content: @escaping () -> Content
) {
self.onOffsetChange = onOffsetChange
self.content = content
}
public var body: some View {
ScrollView {
offsetReader
content()
.padding(.top, -8)
}
.coordinateSpace(name: "frameLayer")
.onPreferenceChange(OffsetPreferenceKey.self, perform: onOffsetChange)
}
var offsetReader: some View {
GeometryReader { proxy in
Color.clear
.preference(
key: OffsetPreferenceKey.self,
value: proxy.frame(in: .named("frameLayer")).minY
)
}
.frame(height: 0)
}
}
private struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value _: inout CGFloat, nextValue _: () -> CGFloat) {}
}

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

@ -0,0 +1,15 @@
import Network
import SwiftUI
@MainActor
public extension View {
func withPreviewsEnv() -> some View {
environment(RouterPath())
.environment(Client(server: ""))
.environment(CurrentAccount.shared)
.environment(UserPreferences.shared)
.environment(CurrentInstance.shared)
.environment(PushNotificationsService.shared)
.environment(QuickLook.shared)
}
}

View file

@ -38,7 +38,15 @@ public enum WindowDestinationMedia: Hashable, Codable {
case mediaViewer(attachments: [MediaAttachment], selectedAttachment: MediaAttachment) case mediaViewer(attachments: [MediaAttachment], selectedAttachment: MediaAttachment)
} }
public enum SheetDestination: Identifiable { public enum SheetDestination: Identifiable, Hashable {
public static func == (lhs: SheetDestination, rhs: SheetDestination) -> Bool {
lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
case newStatusEditor(visibility: Models.Visibility) case newStatusEditor(visibility: Models.Visibility)
case editStatusEditor(status: Status) case editStatusEditor(status: Status)
case replyToStatusEditor(status: Status) case replyToStatusEditor(status: Status)
@ -60,11 +68,13 @@ public enum SheetDestination: Identifiable {
case shareImage(image: UIImage, status: Status) case shareImage(image: UIImage, status: Status)
case editTagGroup(tagGroup: TagGroup, onSaved: ((TagGroup) -> Void)?) case editTagGroup(tagGroup: TagGroup, onSaved: ((TagGroup) -> Void)?)
case timelineContentFilter case timelineContentFilter
case accountEditInfo
case accountFiltersList
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"
@ -90,6 +100,10 @@ public enum SheetDestination: Identifiable {
"settings" "settings"
case .timelineContentFilter: case .timelineContentFilter:
"timelineContentFilter" "timelineContentFilter"
case .accountEditInfo:
"accountEditInfo"
case .accountFiltersList:
"accountFiltersList"
} }
} }
} }
@ -147,8 +161,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

@ -32,9 +32,9 @@ public struct ExploreView: View {
} else if !viewModel.searchQuery.isEmpty { } else if !viewModel.searchQuery.isEmpty {
if let results = viewModel.results[viewModel.searchQuery] { if let results = viewModel.results[viewModel.searchQuery] {
if results.isEmpty, !viewModel.isSearching { if results.isEmpty, !viewModel.isSearching {
EmptyView(iconName: "magnifyingglass", PlaceholderView(iconName: "magnifyingglass",
title: "explore.search.empty.title", title: "explore.search.empty.title",
message: "explore.search.empty.message") message: "explore.search.empty.message")
.listRowBackground(theme.secondaryBackgroundColor) .listRowBackground(theme.secondaryBackgroundColor)
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
} else { } else {
@ -53,12 +53,12 @@ public struct ExploreView: View {
.id(UUID()) .id(UUID())
} }
} else if viewModel.allSectionsEmpty { } else if viewModel.allSectionsEmpty {
EmptyView(iconName: "magnifyingglass", PlaceholderView(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

@ -61,6 +61,7 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
public let source: Source? public let source: Source?
public let bot: Bool public let bot: Bool
public let discoverable: Bool? public let discoverable: Bool?
public let moved: Account?
public var haveAvatar: Bool { public var haveAvatar: Bool {
avatar.lastPathComponent != "missing.png" avatar.lastPathComponent != "missing.png"
@ -70,7 +71,7 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
header.lastPathComponent != "missing.png" header.lastPathComponent != "missing.png"
} }
public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil) { public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil, moved: Account? = nil) {
self.id = id self.id = id
self.username = username self.username = username
self.displayName = displayName self.displayName = displayName
@ -90,14 +91,15 @@ 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
self.moved = moved
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
@ -118,33 +120,36 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
case source case source
case bot case bot
case discoverable case discoverable
case moved
} }
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)
moved = try container.decodeIfPresent(Account.self, forKey: .moved)
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"))
} }

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