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

View file

@ -30,13 +30,13 @@ struct AppView: View {
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
} }
@ -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
} }
@ -106,38 +106,38 @@ struct AppView: View {
} }
#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

@ -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

@ -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,7 +1,7 @@
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 {

View file

@ -1,8 +1,8 @@
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 {

View file

@ -60,9 +60,9 @@ struct NotificationsTab: View {
} }
} }
} }
.onChange(of: selectedTab, { _, newValue in .onChange(of: selectedTab) { _, _ in
clearNotifications() clearNotifications()
}) }
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in .onChange(of: pushNotificationsService.handledNotification) { _, newValue in
if let newValue, let type = newValue.notification.supportedType { if let newValue, let type = newValue.notification.supportedType {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {

View file

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

View file

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

View file

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

View file

@ -5,8 +5,8 @@ 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 {
@ -41,7 +41,7 @@ struct ContentSettingsView: View {
} }
} }
} }
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
@ -142,8 +142,8 @@ struct ContentSettingsView: View {
} }
.navigationTitle("settings.content.navigation-title") .navigationTitle("settings.content.navigation-title")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
#endif #endif
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
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
@ -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,8 +1,8 @@
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
@ -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

@ -28,8 +28,8 @@ 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
} }

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

@ -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

@ -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,8 +1,8 @@
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
@ -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

@ -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

@ -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()
} }
} }

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:
@ -212,7 +212,7 @@ struct TimelineTab: View {
@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,7 +222,7 @@ 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")

View file

@ -1,7 +1,7 @@
import SwiftUI
import Env
import AppAccount import AppAccount
import DesignSystem import DesignSystem
import Env
import SwiftUI
@MainActor @MainActor
struct ToolbarTab: ToolbarContent { struct ToolbarTab: ToolbarContent {
@ -17,7 +17,8 @@ struct ToolbarTab: ToolbarContent {
statusEditorToolbarItem(routerPath: routerPath, statusEditorToolbarItem(routerPath: routerPath,
visibility: userPreferences.postVisibility) visibility: userPreferences.postVisibility)
if UIDevice.current.userInterfaceIdiom != .pad || if UIDevice.current.userInterfaceIdiom != .pad ||
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact) { (UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
{
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
AppAccountsSelectorView(routerPath: routerPath) AppAccountsSelectorView(routerPath: routerPath)
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

@ -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
} }
} }
} }
@ -147,9 +147,9 @@ public struct AccountsListView: View {
case let .error(error): case let .error(error):
Text(error.localizedDescription) Text(error.localizedDescription)
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }
} }
} }

View file

@ -1,8 +1,8 @@
import Models import Models
import Network import Network
import Observation import Observation
import SwiftUI
import OSLog import OSLog
import SwiftUI
public enum AccountsListMode { public enum AccountsListMode {
case following(accountId: String), followers(accountId: String) case following(accountId: String), followers(accountId: String)
@ -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 {
@ -14,7 +14,7 @@ public struct EditAccountView: View {
@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()
} }
} }
} }

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 {
@ -33,12 +33,13 @@ import StatusKit
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
@ -113,11 +114,11 @@ import StatusKit
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
@ -128,11 +129,11 @@ import StatusKit
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
@ -145,8 +146,8 @@ import StatusKit
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,7 +1,7 @@
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
@ -43,4 +43,3 @@ public struct ListsListView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
} }

View file

@ -1,9 +1,9 @@
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 {
@ -24,27 +24,27 @@ public struct AccountStatusesListView: View {
} }
.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,13 +1,13 @@
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 {
@ -40,7 +40,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
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 {
@ -63,11 +63,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
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,7 +1,7 @@
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
@ -12,9 +12,9 @@ public struct FollowedTagsListView: View {
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

@ -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 {

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

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

View file

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

View file

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

View file

@ -197,7 +197,7 @@ import SwiftUI
// better against the tintColor // better against the tintColor
private func computeContrastingTintColor() { private func computeContrastingTintColor() {
func luminance(_ color: Color.Resolved) -> Float { func luminance(_ color: Color.Resolved) -> Float {
return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue; return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue
} }
let resolvedTintColor = tintColor.resolve(in: .init()) let resolvedTintColor = tintColor.resolve(in: .init())
@ -340,7 +340,7 @@ import SwiftUI
ConstellationLight(), ConstellationLight(),
ConstellationDark(), ConstellationDark(),
ThreadsLight(), ThreadsLight(),
ThreadsDark() ThreadsDark(),
] ]
} }

View file

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

View file

@ -3,7 +3,7 @@ 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) {

View file

@ -4,7 +4,7 @@ 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

View file

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

View file

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

View file

@ -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))
} }
} }
@ -159,19 +159,19 @@ import OSLog
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

@ -183,7 +183,6 @@ import SwiftUI
} }
} }
public var alwaysUseDeepl: Bool { public var alwaysUseDeepl: Bool {
didSet { didSet {
storage.alwaysUseDeepl = alwaysUseDeepl storage.alwaysUseDeepl = alwaysUseDeepl
@ -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]) {

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 {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
import AVKit import AVKit
import DesignSystem import DesignSystem
import Env import Env
import Models
import Observation import Observation
import SwiftUI import SwiftUI
import Models
@MainActor @MainActor
@Observable public class MediaUIAttachmentVideoViewModel { @Observable public class MediaUIAttachmentVideoViewModel {
@ -21,9 +21,9 @@ import Models
player = .init(url: url) player = .init(url: url)
player?.audiovisualBackgroundPlaybackPolicy = .pauses player?.audiovisualBackgroundPlaybackPolicy = .pauses
#if !os(visionOS) #if !os(visionOS)
player?.preventsDisplaySleepDuringVideoPlayback = false player?.preventsDisplaySleepDuringVideoPlayback = false
#endif #endif
if (autoPlay || forceAutoPlay) && !isCompact { if autoPlay || forceAutoPlay, !isCompact {
player?.play() player?.play()
isPlaying = true isPlaying = true
} else { } else {
@ -70,7 +70,7 @@ import Models
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

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

View file

@ -15,7 +15,7 @@ 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))
} }
} }

View file

@ -39,7 +39,7 @@ 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

@ -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

@ -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 {
@ -286,7 +286,8 @@ import OSLog
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

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

View file

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

View file

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

View file

@ -164,7 +164,7 @@ import SwiftUI
Task { Task {
do { do {
let _: Marker = try await client.post(endpoint: Markers.markNotifications(lastReadId: id)) let _: Marker = try await client.post(endpoint: Markers.markNotifications(lastReadId: id))
} catch { } } catch {}
} }
} }

View file

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

View file

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

View file

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

View file

@ -1,12 +1,11 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Foundation import Foundation
import SwiftUI
import Models import Models
import SwiftData import SwiftData
import SwiftUI
extension StatusEditor { extension StatusEditor {
@MainActor @MainActor
struct AutoCompleteView: View { struct AutoCompleteView: View {
@Environment(\.modelContext) var context @Environment(\.modelContext) var context
@ -21,8 +20,9 @@ extension StatusEditor {
var body: some View { var body: some View {
if !viewModel.mentionsSuggestions.isEmpty || if !viewModel.mentionsSuggestions.isEmpty ||
!viewModel.tagsSuggestions.isEmpty || !viewModel.tagsSuggestions.isEmpty ||
(viewModel.showRecentsTagsInline && !recentTags.isEmpty) { (viewModel.showRecentsTagsInline && !recentTags.isEmpty)
{
VStack { VStack {
HStack { HStack {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {

View file

@ -1,10 +1,10 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Env
import Foundation import Foundation
import SwiftUI
import Models import Models
import SwiftData import SwiftData
import Env import SwiftUI
extension StatusEditor.AutoCompleteView { extension StatusEditor.AutoCompleteView {
@MainActor @MainActor

View file

@ -1,12 +1,11 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Foundation import Foundation
import SwiftUI
import Models import Models
import SwiftData import SwiftData
import SwiftUI
extension StatusEditor.AutoCompleteView {
extension StatusEditor.AutoCompleteView {
struct MentionsView: View { struct MentionsView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme

View file

@ -1,12 +1,11 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Foundation import Foundation
import SwiftUI
import Models import Models
import SwiftData import SwiftData
import SwiftUI
extension StatusEditor.AutoCompleteView {
extension StatusEditor.AutoCompleteView {
struct RecentTagsView: View { struct RecentTagsView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme

View file

@ -1,10 +1,9 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Foundation import Foundation
import SwiftUI
import Models import Models
import SwiftData import SwiftData
import SwiftUI
extension StatusEditor.AutoCompleteView { extension StatusEditor.AutoCompleteView {
struct RemoteTagsView: View { struct RemoteTagsView: View {

View file

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

View file

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

View file

@ -2,9 +2,9 @@ import AVFoundation
import Foundation import Foundation
import UIKit import UIKit
extension StatusEditor { public extension StatusEditor {
public actor Compressor { actor Compressor {
public init() { } public init() {}
enum CompressorError: Error { enum CompressorError: Error {
case noData case noData
@ -106,5 +106,4 @@ extension StatusEditor {
} }
} }
} }
} }

View file

@ -1,11 +1,10 @@
import DesignSystem import DesignSystem
import Env import Env
import SwiftUI
import Models import Models
import NukeUI import NukeUI
import SwiftUI
extension StatusEditor { extension StatusEditor {
@MainActor @MainActor
struct CustomEmojisView: View { struct CustomEmojisView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss

View file

@ -1,53 +1,53 @@
#if !os(visionOS) && !DEBUG #if !os(visionOS) && !DEBUG
import DesignSystem import DesignSystem
import GiphyUISDK import GiphyUISDK
import SwiftUI import SwiftUI
import UIKit import UIKit
struct GifPickerView: UIViewControllerRepresentable { struct GifPickerView: UIViewControllerRepresentable {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
var completion: (String) -> Void var completion: (String) -> Void
var onShouldDismissGifPicker: () -> Void var onShouldDismissGifPicker: () -> Void
func makeUIViewController(context: Context) -> GiphyViewController { func makeUIViewController(context: Context) -> GiphyViewController {
Giphy.configure(apiKey: "MIylJkNX57vcUNZxmSODKU9dQKBgXCkV") Giphy.configure(apiKey: "MIylJkNX57vcUNZxmSODKU9dQKBgXCkV")
let controller = GiphyViewController() let controller = GiphyViewController()
controller.swiftUIEnabled = true controller.swiftUIEnabled = true
controller.mediaTypeConfig = [.gifs, .stickers, .recents] controller.mediaTypeConfig = [.gifs, .stickers, .recents]
controller.delegate = context.coordinator controller.delegate = context.coordinator
controller.navigationController?.isNavigationBarHidden = true controller.navigationController?.isNavigationBarHidden = true
controller.navigationController?.setNavigationBarHidden(true, animated: false) controller.navigationController?.setNavigationBarHidden(true, animated: false)
GiphyViewController.trayHeightMultiplier = 1.0 GiphyViewController.trayHeightMultiplier = 1.0
controller.theme = GPHTheme(type: theme.selectedScheme == .dark ? .darkBlur : .lightBlur) controller.theme = GPHTheme(type: theme.selectedScheme == .dark ? .darkBlur : .lightBlur)
return controller return controller
}
func updateUIViewController(_: UIViewControllerType, context _: Context) {}
func makeCoordinator() -> Coordinator {
GifPickerView.Coordinator(parent: self)
}
class Coordinator: NSObject, GiphyDelegate {
var parent: GifPickerView
init(parent: GifPickerView) {
self.parent = parent
} }
func didDismiss(controller _: GiphyViewController?) { func updateUIViewController(_: UIViewControllerType, context _: Context) {}
parent.onShouldDismissGifPicker()
func makeCoordinator() -> Coordinator {
GifPickerView.Coordinator(parent: self)
} }
func didSelectMedia(giphyViewController _: GiphyViewController, media: GPHMedia) { class Coordinator: NSObject, GiphyDelegate {
let url = media.url(rendition: .fixedWidth, fileType: .gif) var parent: GifPickerView
parent.completion(url ?? "")
init(parent: GifPickerView) {
self.parent = parent
}
func didDismiss(controller _: GiphyViewController?) {
parent.onShouldDismissGifPicker()
}
func didSelectMedia(giphyViewController _: GiphyViewController, media: GPHMedia) {
let url = media.url(rendition: .fixedWidth, fileType: .gif)
parent.completion(url ?? "")
}
} }
} }
}
#endif #endif

View file

@ -1,10 +1,9 @@
import DesignSystem import DesignSystem
import Env import Env
import SwiftUI
import Models import Models
import SwiftUI
extension StatusEditor { extension StatusEditor {
@MainActor @MainActor
struct LangButton: View { struct LangButton: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme

View file

@ -13,5 +13,4 @@ extension StatusEditor {
let mediaAttachment: MediaAttachment? let mediaAttachment: MediaAttachment?
let error: Error? let error: Error?
} }
} }

View file

@ -171,5 +171,4 @@ extension StatusEditor {
return translation?.content.asRawText return translation?.content.asRawText
} }
} }
} }

View file

@ -249,5 +249,4 @@ extension StatusEditor {
.cornerRadius(8) .cornerRadius(8)
} }
} }
} }

View file

@ -121,5 +121,4 @@ extension StatusEditor {
return index == count - 1 && count < maxEntries return index == count - 1 && count < maxEntries
} }
} }
} }

View file

@ -86,10 +86,10 @@ extension StatusEditor {
static var transferRepresentation: some TransferRepresentation { static var transferRepresentation: some TransferRepresentation {
FileRepresentation(importedContentType: .movie) { receivedTransferrable in FileRepresentation(importedContentType: .movie) { receivedTransferrable in
return MovieFileTranseferable(url: receivedTransferrable.localURL) MovieFileTranseferable(url: receivedTransferrable.localURL)
} }
FileRepresentation(importedContentType: .video) { receivedTransferrable in FileRepresentation(importedContentType: .video) { receivedTransferrable in
return MovieFileTranseferable(url: receivedTransferrable.localURL) MovieFileTranseferable(url: receivedTransferrable.localURL)
} }
} }
} }
@ -112,7 +112,7 @@ extension StatusEditor {
static var transferRepresentation: some TransferRepresentation { static var transferRepresentation: some TransferRepresentation {
FileRepresentation(importedContentType: .gif) { receivedTransferrable in FileRepresentation(importedContentType: .gif) { receivedTransferrable in
return GifFileTranseferable(url: receivedTransferrable.localURL) GifFileTranseferable(url: receivedTransferrable.localURL)
} }
} }
} }
@ -133,7 +133,7 @@ public extension StatusEditor {
public static var transferRepresentation: some TransferRepresentation { public static var transferRepresentation: some TransferRepresentation {
FileRepresentation(importedContentType: .image) { receivedTransferrable in FileRepresentation(importedContentType: .image) { receivedTransferrable in
return ImageFileTranseferable(url: receivedTransferrable.localURL) ImageFileTranseferable(url: receivedTransferrable.localURL)
} }
} }
} }
@ -141,15 +141,15 @@ public extension StatusEditor {
public extension ReceivedTransferredFile { public extension ReceivedTransferredFile {
var localURL: URL { var localURL: URL {
if self.isOriginalFile { if isOriginalFile {
return file return file
} }
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(self.file.pathExtension)") let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(file.pathExtension)")
try? FileManager.default.copyItem(at: self.file, to: copy) try? FileManager.default.copyItem(at: file, to: copy)
return copy return copy
} }
} }
public extension URL { public extension URL {
func mimeType() -> String { func mimeType() -> String {
if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType { if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType {

View file

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

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