iOS 17+ only support + migrating to Observation framework (#1571)

* Initial iOS 17 + Observable migration

* More Observation

* More observation

* Checkpoint

* Checkpoint

* Bump version to 1.8.0

* SwiftFormat

* Fix home timeline switch on login

* Fix sidebar routerPath

* Fixes on detail view

* Remove print changes

* Simply detail view

* More opt

* Migrate DisplaySettingsLocalValues

* Better post detail transition

* Status detail animation finally right

* Cleanup
This commit is contained in:
Thomas Ricouard 2023-09-18 07:01:23 +02:00 committed by GitHub
parent 3853eff065
commit 4189a59cf6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
116 changed files with 820 additions and 812 deletions

View file

@ -975,13 +975,13 @@
INFOPLIST_FILE = IceCubesNotifications/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.9;
MARKETING_VERSION = 1.8.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1005,13 +1005,13 @@
INFOPLIST_FILE = IceCubesNotifications/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.9;
MARKETING_VERSION = 1.8.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1036,13 +1036,13 @@
INFOPLIST_FILE = IceCubesShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.9;
MARKETING_VERSION = 1.8.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1066,13 +1066,13 @@
INFOPLIST_FILE = IceCubesShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.9;
MARKETING_VERSION = 1.8.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1240,11 +1240,11 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.7.9;
MARKETING_VERSION = 1.8.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
@ -1293,11 +1293,11 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.7.9;
MARKETING_VERSION = 1.8.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
@ -1325,13 +1325,13 @@
INFOPLIST_FILE = IceCubesActionExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.9;
MARKETING_VERSION = 1.8.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1356,13 +1356,13 @@
INFOPLIST_FILE = IceCubesActionExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.9;
MARKETING_VERSION = 1.8.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;

View file

@ -112,13 +112,13 @@ extension View {
}
func withEnvironments() -> some View {
environmentObject(CurrentAccount.shared)
environment(CurrentAccount.shared)
.environmentObject(UserPreferences.shared)
.environmentObject(CurrentInstance.shared)
.environment(CurrentInstance.shared)
.environmentObject(Theme.shared)
.environmentObject(AppAccountsManager.shared)
.environmentObject(PushNotificationsService.shared)
.environmentObject(AppAccountsManager.shared.currentClient)
.environment(AppAccountsManager.shared)
.environment(PushNotificationsService.shared)
.environment(AppAccountsManager.shared.currentClient)
}
}

View file

@ -15,15 +15,15 @@ struct IceCubesApp: App {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var appAccountsManager = AppAccountsManager.shared
@StateObject private var currentInstance = CurrentInstance.shared
@StateObject private var currentAccount = CurrentAccount.shared
@State private var appAccountsManager = AppAccountsManager.shared
@State private var currentInstance = CurrentInstance.shared
@State private var currentAccount = CurrentAccount.shared
@StateObject private var userPreferences = UserPreferences.shared
@StateObject private var pushNotificationsService = PushNotificationsService.shared
@StateObject private var watcher = StreamWatcher()
@StateObject private var quickLook = QuickLook()
@State private var pushNotificationsService = PushNotificationsService.shared
@State private var watcher = StreamWatcher()
@State private var quickLook = QuickLook()
@StateObject private var theme = Theme.shared
@StateObject private var sidebarRouterPath = RouterPath()
@State private var sidebarRouterPath = RouterPath()
@State private var selectedTab: Tab = .timeline
@State private var popToRootTab: Tab = .other
@ -43,32 +43,32 @@ struct IceCubesApp: App {
setupRevenueCat()
refreshPushSubs()
}
.environmentObject(appAccountsManager)
.environmentObject(appAccountsManager.currentClient)
.environmentObject(quickLook)
.environmentObject(currentAccount)
.environmentObject(currentInstance)
.environment(appAccountsManager)
.environment(appAccountsManager.currentClient)
.environment(quickLook)
.environment(currentAccount)
.environment(currentInstance)
.environmentObject(userPreferences)
.environmentObject(theme)
.environmentObject(watcher)
.environmentObject(pushNotificationsService)
.environment(watcher)
.environment(pushNotificationsService)
.environment(\.isSupporter, isSupporter)
.fullScreenCover(item: $quickLook.url, content: { url in
QuickLookPreview(selectedURL: url, urls: quickLook.urls)
.edgesIgnoringSafeArea(.bottom)
.background(TransparentBackground())
})
.onChange(of: pushNotificationsService.handledNotification) { notification in
if notification != nil {
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
if newValue != nil {
pushNotificationsService.handledNotification = nil
if appAccountsManager.currentAccount.oauthToken?.accessToken != notification?.account.token.accessToken,
if appAccountsManager.currentAccount.oauthToken?.accessToken != newValue?.account.token.accessToken,
let account = appAccountsManager.availableAccounts.first(where:
{ $0.oauthToken?.accessToken == notification?.account.token.accessToken })
{ $0.oauthToken?.accessToken == newValue?.account.token.accessToken })
{
appAccountsManager.currentAccount = account
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
selectedTab = .notifications
pushNotificationsService.handledNotification = notification
pushNotificationsService.handledNotification = newValue
}
} else {
selectedTab = .notifications
@ -79,12 +79,12 @@ struct IceCubesApp: App {
.commands {
appMenu
}
.onChange(of: scenePhase) { scenePhase in
handleScenePhase(scenePhase: scenePhase)
.onChange(of: scenePhase) { _, newValue in
handleScenePhase(scenePhase: newValue)
}
.onChange(of: appAccountsManager.currentClient) { newClient in
setNewClientsInEnv(client: newClient)
if newClient.isAuth {
.onChange(of: appAccountsManager.currentClient) { _, newValue in
setNewClientsInEnv(client: newValue)
if newValue.isAuth {
watcher.watch(streams: [.user, .direct])
}
}
@ -111,8 +111,7 @@ struct IceCubesApp: App {
private var sidebarView: some View {
SideBarView(selectedTab: $selectedTab,
popToRootTab: $popToRootTab,
tabs: availableTabs,
routerPath: sidebarRouterPath)
tabs: availableTabs)
{
GeometryReader { _ in
HStack(spacing: 0) {
@ -143,9 +142,10 @@ struct IceCubesApp: App {
}
}
}
}.onChange(of: $appAccountsManager.currentAccount.id) { _ in
}.onChange(of: $appAccountsManager.currentAccount.id) {
sideBarLoadedTabs.removeAll()
}
.environment(sidebarRouterPath)
}
private var notificationsSecondaryColumn: some View {
@ -218,7 +218,7 @@ struct IceCubesApp: App {
watcher.stopWatching()
case .active:
watcher.watch(streams: [.user, .direct])
UIApplication.shared.applicationIconBadgeNumber = 0
UNUserNotificationCenter.current().setBadgeCount(0)
Task {
await userPreferences.refreshServerPreferences()
}

View file

@ -9,7 +9,7 @@ public struct ReportView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
let status: Status
@State private var commentText: String = ""

View file

@ -1,5 +1,6 @@
import DesignSystem
import Env
import Observation
import SafariServices
import SwiftUI
@ -13,9 +14,9 @@ extension View {
private struct SafariRouter: ViewModifier {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var routerPath: RouterPath
@Environment(RouterPath.self) private var routerPath
@StateObject private var safariManager = InAppSafariManager()
@State private var safariManager = InAppSafariManager()
func body(content: Content) -> some View {
content
@ -58,7 +59,7 @@ private struct SafariRouter: ViewModifier {
}
@MainActor
private class InAppSafariManager: NSObject, ObservableObject, SFSafariViewControllerDelegate {
@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
var windowScene: UIWindowScene?
let viewController: UIViewController = .init()
var window: UIWindow?

View file

@ -6,16 +6,16 @@ import Models
import SwiftUI
struct SideBarView<Content: View>: View {
@EnvironmentObject private var appAccounts: AppAccountsManager
@EnvironmentObject private var currentAccount: CurrentAccount
@Environment(AppAccountsManager.self) private var appAccounts
@Environment(CurrentAccount.self) private var currentAccount
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var watcher: StreamWatcher
@Environment(StreamWatcher.self) private var watcher
@EnvironmentObject private var userPreferences: UserPreferences
@Environment(RouterPath.self) private var routerPath
@Binding var selectedTab: Tab
@Binding var popToRootTab: Tab
var tabs: [Tab]
@ObservedObject var routerPath = RouterPath()
@ViewBuilder var content: () -> Content
private func badgeFor(tab: Tab) -> Int {
@ -122,6 +122,7 @@ struct SideBarView<Content: View>: View {
}
var body: some View {
@Bindable var routerPath = routerPath
HStack(spacing: 0) {
ScrollView {
VStack(alignment: .center) {

View file

@ -10,9 +10,9 @@ import SwiftUI
struct ExploreTab: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var client: Client
@StateObject private var routerPath = RouterPath()
@Environment(CurrentAccount.self) private var currentAccount
@Environment(Client.self) private var client
@State private var routerPath = RouterPath()
@Binding var popToRootTab: Tab
var body: some View {
@ -35,13 +35,13 @@ struct ExploreTab: View {
}
}
.withSafariRouter()
.environmentObject(routerPath)
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .explore {
.environment(routerPath)
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
if newValue == .explore {
routerPath.path = []
}
}
.onChange(of: client.id) { _ in
.onChange(of: client.id) {
routerPath.path = []
}
.onAppear {

View file

@ -10,11 +10,11 @@ import SwiftUI
struct MessagesTab: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var client: Client
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var appAccount: AppAccountsManager
@StateObject private var routerPath = RouterPath()
@Environment(StreamWatcher.self) private var watcher
@Environment(Client.self) private var client
@Environment(CurrentAccount.self) private var currentAccount
@Environment(AppAccountsManager.self) private var appAccount
@State private var routerPath = RouterPath()
@Binding var popToRootTab: Tab
var body: some View {
@ -32,18 +32,18 @@ struct MessagesTab: View {
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
.id(client.id)
}
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .messages {
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
if newValue == .messages {
routerPath.path = []
}
}
.onChange(of: client.id) { _ in
.onChange(of: client.id) {
routerPath.path = []
}
.onAppear {
routerPath.client = client
}
.withSafariRouter()
.environmentObject(routerPath)
.environment(routerPath)
}
}

View file

@ -12,13 +12,13 @@ struct NotificationsTab: View {
@Environment(\.scenePhase) private var scenePhase
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var appAccount: AppAccountsManager
@EnvironmentObject private var currentAccount: CurrentAccount
@Environment(Client.self) private var client
@Environment(StreamWatcher.self) private var watcher
@Environment(AppAccountsManager.self) private var appAccount
@Environment(CurrentAccount.self) private var currentAccount
@EnvironmentObject private var userPreferences: UserPreferences
@EnvironmentObject private var pushNotificationsService: PushNotificationsService
@StateObject private var routerPath = RouterPath()
@Environment(PushNotificationsService.self) private var pushNotificationsService
@State private var routerPath = RouterPath()
@Binding var popToRootTab: Tab
let lockedType: Models.Notification.NotificationType?
@ -54,35 +54,35 @@ struct NotificationsTab: View {
}
}
.withSafariRouter()
.environmentObject(routerPath)
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .notifications {
.environment(routerPath)
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
if newValue == .notifications {
routerPath.path = []
}
}
.onChange(of: pushNotificationsService.handledNotification) { notification in
if let notification, let type = notification.notification.supportedType {
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
if let newValue, let type = newValue.notification.supportedType {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
switch type {
case .follow, .follow_request:
routerPath.navigate(to: .accountDetailWithAccount(account: notification.notification.account))
routerPath.navigate(to: .accountDetailWithAccount(account: newValue.notification.account))
default:
if let status = notification.notification.status {
if let status = newValue.notification.status {
routerPath.navigate(to: .statusDetailWithStatus(status: status))
}
}
}
}
}
.onChange(of: scenePhase, perform: { scenePhase in
switch scenePhase {
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:
clearNotifications()
default:
break
}
})
.onChange(of: client.id) { _ in
}
.onChange(of: client.id) {
routerPath.path = []
}
}

View file

@ -9,11 +9,11 @@ import Shimmer
import SwiftUI
struct ProfileTab: View {
@EnvironmentObject private var appAccount: AppAccountsManager
@Environment(AppAccountsManager.self) private var appAccount
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@EnvironmentObject private var currentAccount: CurrentAccount
@StateObject private var routerPath = RouterPath()
@Environment(Client.self) private var client
@Environment(CurrentAccount.self) private var currentAccount
@State private var routerPath = RouterPath()
@Binding var popToRootTab: Tab
var body: some View {
@ -29,18 +29,18 @@ struct ProfileTab: View {
.redacted(reason: .placeholder)
}
}
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .profile {
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
if newValue == .profile {
routerPath.path = []
}
}
.onChange(of: client.id) { _ in
.onChange(of: client.id) {
routerPath.path = []
}
.onAppear {
routerPath.client = client
}
.withSafariRouter()
.environmentObject(routerPath)
.environment(routerPath)
}
}

View file

@ -3,7 +3,7 @@ import Env
import SwiftUI
struct AboutView: View {
@EnvironmentObject private var routerPath: RouterPath
@Environment(RouterPath.self) private var routerPath
@EnvironmentObject private var theme: Theme
let versionNumber: String

View file

@ -11,12 +11,12 @@ struct AccountSettingsView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.openURL) private var openURL
@EnvironmentObject private var pushNotifications: PushNotificationsService
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var currentInstance: CurrentInstance
@Environment(PushNotificationsService.self) private var pushNotifications
@Environment(CurrentAccount.self) private var currentAccount
@Environment(CurrentInstance.self) private var currentInstance
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var appAccountsManager: AppAccountsManager
@EnvironmentObject private var client: Client
@Environment(AppAccountsManager.self) private var appAccountsManager
@Environment(Client.self) private var client
@State private var isEditingAccount: Bool = false
@State private var isEditingFilters: Bool = false

View file

@ -13,10 +13,10 @@ struct AddAccountView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.scenePhase) private var scenePhase
@EnvironmentObject private var appAccountsManager: AppAccountsManager
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var pushNotifications: PushNotificationsService
@Environment(AppAccountsManager.self) private var appAccountsManager
@Environment(CurrentAccount.self) private var currentAccount
@Environment(CurrentInstance.self) private var currentInstance
@Environment(PushNotificationsService.self) private var pushNotifications
@EnvironmentObject private var theme: Theme
@State private var instanceName: String = ""
@ -89,7 +89,7 @@ struct AddAccountView: View {
}
isSigninIn = false
}
.onChange(of: instanceName) { newValue in
.onChange(of: instanceName) { _, newValue in
instanceNamePublisher.send(newValue)
}
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { _ in
@ -119,24 +119,24 @@ struct AddAccountView: View {
}
}
}
.onChange(of: scenePhase, perform: { scenePhase in
switch scenePhase {
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:
isSigninIn = false
default:
break
}
})
}
.onOpenURL(perform: { url in
Task {
await continueSignIn(url: url)
}
})
.onChange(of: oauthURL, perform: { newValue in
.onChange(of: oauthURL) { _, newValue in
if newValue == nil {
isSigninIn = false
}
})
}
.sheet(item: $oauthURL, content: { url in
SafariView(url: url)
})

View file

@ -44,7 +44,7 @@ struct ContentSettingsView: View {
}
}
.listRowBackground(theme.primaryBackgroundColor)
.onChange(of: userPreferences.useInstanceContentSettings) { newVal in
.onChange(of: userPreferences.useInstanceContentSettings) { _, newVal in
if newVal {
userPreferences.appAutoExpandSpoilers = userPreferences.autoExpandSpoilers
userPreferences.appAutoExpandMedia = userPreferences.autoExpandMedia
@ -93,7 +93,7 @@ struct ContentSettingsView: View {
}
}
}
.onChange(of: userPreferences.postVisibility) { _ in
.onChange(of: userPreferences.postVisibility) {
userPreferences.conformReplyVisibilityConstraints()
}

View file

@ -3,47 +3,20 @@ import DesignSystem
import Env
import Models
import Network
import Observation
import Status
import SwiftUI
class DisplaySettingsLocalValues: ObservableObject {
@Published var tintColor = Theme.shared.tintColor
@Published var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
@Published var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor
@Published var labelColor = Theme.shared.labelColor
@Published var lineSpacing = Theme.shared.lineSpacing
@Published var fontSizeScale = Theme.shared.fontSizeScale
@Observable class DisplaySettingsLocalValues {
var tintColor = Theme.shared.tintColor
var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor
var labelColor = Theme.shared.labelColor
var lineSpacing = Theme.shared.lineSpacing
var fontSizeScale = Theme.shared.fontSizeScale
private let debouncesDelay: DispatchQueue.SchedulerTimeType.Stride = .seconds(0.5)
private var subscriptions = Set<AnyCancellable>()
init() {
$tintColor
.debounce(for: debouncesDelay, scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.tintColor = newColor })
.store(in: &subscriptions)
$primaryBackgroundColor
.debounce(for: debouncesDelay, scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.primaryBackgroundColor = newColor })
.store(in: &subscriptions)
$secondaryBackgroundColor
.debounce(for: debouncesDelay, scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.secondaryBackgroundColor = newColor })
.store(in: &subscriptions)
$labelColor
.debounce(for: debouncesDelay, scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.labelColor = newColor })
.store(in: &subscriptions)
$lineSpacing
.debounce(for: debouncesDelay, scheduler: DispatchQueue.main)
.sink(receiveValue: { newSpacing in Theme.shared.lineSpacing = newSpacing })
.store(in: &subscriptions)
$fontSizeScale
.debounce(for: debouncesDelay, scheduler: DispatchQueue.main)
.sink(receiveValue: { newScale in Theme.shared.fontSizeScale = newScale })
.store(in: &subscriptions)
}
init() { }
}
struct DisplaySettingsView: View {
@ -53,7 +26,7 @@ struct DisplaySettingsView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var userPreferences: UserPreferences
@StateObject private var localValues = DisplaySettingsLocalValues()
@State private var localValues = DisplaySettingsLocalValues()
@State private var isFontSelectorPresented = false
@ -64,7 +37,7 @@ struct DisplaySettingsView: View {
var body: some View {
ZStack(alignment: .top) {
Form {
StatusRowView(viewModel: { previewStatusViewModel })
StatusRowView(viewModel: previewStatusViewModel)
.allowsHitTesting(false)
.opacity(0)
.hidden()
@ -77,13 +50,37 @@ struct DisplaySettingsView: View {
.navigationTitle("settings.display.navigation-title")
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.task(id: localValues.tintColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch { }
Theme.shared.tintColor = localValues.tintColor
}
.task(id: localValues.primaryBackgroundColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch { }
Theme.shared.primaryBackgroundColor = localValues.primaryBackgroundColor
}
.task(id: localValues.secondaryBackgroundColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch { }
Theme.shared.secondaryBackgroundColor = localValues.secondaryBackgroundColor
}
.task(id: localValues.labelColor) {
do { try await Task.sleep(for: .microseconds(500)) } catch { }
Theme.shared.labelColor = localValues.labelColor
}
.task(id: localValues.lineSpacing) {
do { try await Task.sleep(for: .microseconds(500)) } catch { }
Theme.shared.lineSpacing = localValues.lineSpacing
}
.task(id: localValues.fontSizeScale) {
do { try await Task.sleep(for: .microseconds(500)) } catch { }
Theme.shared.fontSizeScale = localValues.fontSizeScale
}
examplePost
}
}
private var examplePost: some View {
VStack(spacing: 0) {
StatusRowView(viewModel: { previewStatusViewModel })
StatusRowView(viewModel: previewStatusViewModel)
.allowsHitTesting(false)
.padding(.layoutPadding)
.background(theme.primaryBackgroundColor)
@ -111,7 +108,7 @@ struct DisplaySettingsView: View {
}
.disabled(theme.followSystemColorScheme)
.opacity(theme.followSystemColorScheme ? 0.5 : 1.0)
.onChange(of: theme.selectedSet) { _ in
.onChange(of: theme.selectedSet) {
localValues.tintColor = theme.tintColor
localValues.primaryBackgroundColor = theme.primaryBackgroundColor
localValues.secondaryBackgroundColor = theme.secondaryBackgroundColor

View file

@ -9,10 +9,10 @@ import UserNotifications
struct PushNotificationsView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var appAccountsManager: AppAccountsManager
@EnvironmentObject private var pushNotifications: PushNotificationsService
@Environment(AppAccountsManager.self) private var appAccountsManager
@Environment(PushNotificationsService.self) private var pushNotifications
@StateObject public var subscription: PushNotificationSubscriptionSettings
@State public var subscription: PushNotificationSubscriptionSettings
var body: some View {
Form {

View file

@ -12,15 +12,14 @@ import Timeline
struct SettingsTabs: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var pushNotifications: PushNotificationsService
@Environment(PushNotificationsService.self) private var pushNotifications
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var client: Client
@EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var appAccountsManager: AppAccountsManager
@Environment(Client.self) private var client
@Environment(CurrentInstance.self) private var currentInstance
@Environment(AppAccountsManager.self) private var appAccountsManager
@EnvironmentObject private var theme: Theme
@StateObject private var routerPath = RouterPath()
@State private var routerPath = RouterPath()
@State private var addAccountSheetPresented = false
@State private var isEditingAccount = false
@State private var cachedRemoved = false
@ -67,9 +66,9 @@ struct SettingsTabs: View {
}
}
.withSafariRouter()
.environmentObject(routerPath)
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .notifications {
.environment(routerPath)
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
if newValue == .notifications {
routerPath.path = []
}
}

View file

@ -14,7 +14,7 @@ struct SwipeActionsSettingsView: View {
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusLeadingLeft,
label: "settings.swipeactions.primary")
.onChange(of: userPreferences.swipeActionsStatusLeadingLeft) { action in
.onChange(of: userPreferences.swipeActionsStatusLeadingLeft) { _, action in
if action == .none {
userPreferences.swipeActionsStatusLeadingRight = .none
}
@ -29,7 +29,7 @@ struct SwipeActionsSettingsView: View {
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusTrailingRight,
label: "settings.swipeactions.primary")
.onChange(of: userPreferences.swipeActionsStatusTrailingRight) { action in
.onChange(of: userPreferences.swipeActionsStatusTrailingRight) { _, action in
if action == .none {
userPreferences.swipeActionsStatusTrailingLeft = .none
}

View file

@ -52,7 +52,9 @@ struct TranslationSettingsView: View {
.navigationTitle("settings.translation.navigation-title")
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.onChange(of: apiKey, perform: writeNewValue)
.onChange(of: apiKey) {
writeNewValue()
}
.onAppear(perform: updatePrefs)
}

View file

@ -58,7 +58,7 @@ struct AddRemoteTimelineView: View {
Button("action.cancel", action: { dismiss() })
}
}
.onChange(of: instanceName) { newValue in
.onChange(of: instanceName) { _, newValue in
instanceNamePublisher.send(newValue)
}
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in

View file

@ -93,7 +93,7 @@ struct EditTagGroupView: View {
.onSubmit {
focusedField = Focus.new
}
.onChange(of: sfSymbolName) { _ in
.onChange(of: sfSymbolName) {
popupTagsPresented = true
}

View file

@ -8,12 +8,12 @@ import SwiftUI
import Timeline
struct TimelineTab: View {
@EnvironmentObject private var appAccount: AppAccountsManager
@Environment(AppAccountsManager.self) private var appAccount
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentAccount: CurrentAccount
@Environment(CurrentAccount.self) private var currentAccount
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var client: Client
@StateObject private var routerPath = RouterPath()
@Environment(Client.self) private var client
@State private var routerPath = RouterPath()
@Binding var popToRootTab: Tab
@State private var didAppear: Bool = false
@ -58,22 +58,22 @@ struct TimelineTab: View {
routerPath.presentedSheet = .addAccount
}
}
.onChange(of: client.isAuth, perform: { _ in
.onChange(of: client.isAuth) {
if client.isAuth {
timeline = lastTimelineFilter
} else {
timeline = .federated
}
})
.onChange(of: currentAccount.account?.id, perform: { _ in
}
.onChange(of: currentAccount.account?.id) {
if client.isAuth, canFilterTimeline {
timeline = lastTimelineFilter
} else {
timeline = .federated
}
})
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .timeline {
}
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
if newValue == .timeline {
if routerPath.path.isEmpty {
scrollToTopSignal += 1
} else {
@ -81,16 +81,16 @@ struct TimelineTab: View {
}
}
}
.onChange(of: client.id) { _ in
.onChange(of: client.id) {
routerPath.path = []
}
.onChange(of: timeline) { timeline in
if timeline == .home || timeline == .federated || timeline == .local {
lastTimelineFilter = timeline
.onChange(of: timeline) { _, newValue in
if client.isAuth, newValue == .home || newValue == .federated || newValue == .local {
lastTimelineFilter = newValue
}
}
.withSafariRouter()
.environmentObject(routerPath)
.environment(routerPath)
}
@ViewBuilder

View file

@ -28,11 +28,11 @@ class ShareViewController: UIViewController {
if let attachments = item.attachments {
let view = StatusEditorView(mode: .shareExtension(items: attachments))
.environmentObject(UserPreferences.shared)
.environmentObject(appAccountsManager)
.environmentObject(client)
.environmentObject(account)
.environment(appAccountsManager)
.environment(client)
.environment(account)
.environmentObject(theme)
.environmentObject(instance)
.environment(instance)
.tint(theme.tintColor)
.preferredColorScheme(colorScheme == .light ? .light : .dark)
let childView = UIHostingController(rootView: view)

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Account",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -3,12 +3,12 @@ import Network
import SwiftUI
public struct AccountDetailContextMenu: View {
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var currentInstance: CurrentInstance
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@Environment(CurrentInstance.self) private var currentInstance
@EnvironmentObject private var preferences: UserPreferences
@ObservedObject var viewModel: AccountDetailViewModel
var viewModel: AccountDetailViewModel
public var body: some View {
if let account = viewModel.account {

View file

@ -12,13 +12,13 @@ struct AccountDetailHeaderView: View {
}
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var quickLook: QuickLook
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var currentAccount: CurrentAccount
@Environment(QuickLook.self) private var quickLook
@Environment(RouterPath.self) private var routerPath
@Environment(CurrentAccount.self) private var currentAccount
@Environment(\.redactionReasons) private var reasons
@Environment(\.isSupporter) private var isSupporter: Bool
@ObservedObject var viewModel: AccountDetailViewModel
var viewModel: AccountDetailViewModel
let account: Account
let scrollViewProxy: ScrollViewProxy?

View file

@ -11,15 +11,15 @@ public struct AccountDetailView: View {
@Environment(\.openURL) private var openURL
@Environment(\.redactionReasons) private var reasons
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var currentInstance: CurrentInstance
@Environment(StreamWatcher.self) private var watcher
@Environment(CurrentAccount.self) private var currentAccount
@Environment(CurrentInstance.self) private var currentInstance
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@StateObject private var viewModel: AccountDetailViewModel
@State private var viewModel: AccountDetailViewModel
@State private var isCurrentUser: Bool = false
@State private var isCreateListAlertPresented: Bool = false
@State private var createListTitle: String = ""
@ -30,12 +30,12 @@ public struct AccountDetailView: View {
/// When coming from a URL like a mention tap in a status.
public init(accountId: String) {
_viewModel = StateObject(wrappedValue: .init(accountId: accountId))
_viewModel = .init(initialValue: .init(accountId: accountId))
}
/// When the account is already fetched by the parent caller.
public init(account: Account) {
_viewModel = StateObject(wrappedValue: .init(account: account))
_viewModel = .init(initialValue: .init(account: account))
}
public var body: some View {
@ -114,21 +114,21 @@ public struct AccountDetailView: View {
SoundEffectManager.shared.playSound(of: .refresh)
}
}
.onChange(of: watcher.latestEvent?.id) { _ in
.onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent,
viewModel.accountId == currentAccount.account?.id
{
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
}
}
.onChange(of: isEditingAccount, perform: { isEditing in
if !isEditing {
.onChange(of: isEditingAccount) { _, newValue in
if !newValue {
Task {
await viewModel.fetchAccount()
await preferences.refreshServerPreferences()
}
}
})
}
.sheet(isPresented: $isEditingAccount, content: {
EditAccountView()
})
@ -292,7 +292,7 @@ public struct AccountDetailView: View {
.listRowSeparator(.hidden)
.listRowBackground(theme.primaryBackgroundColor)
ForEach(viewModel.pinned) { status in
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
}
Rectangle()
.fill(theme.secondaryBackgroundColor)

View file

@ -1,11 +1,12 @@
import Env
import Models
import Network
import Observation
import Status
import SwiftUI
@MainActor
class AccountDetailViewModel: ObservableObject, StatusesFetcher {
@Observable class AccountDetailViewModel: StatusesFetcher {
let accountId: String
var client: Client?
var isCurrentUser: Bool = false
@ -56,8 +57,8 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
case lists
}
@Published var accountState: AccountState = .loading
@Published var tabState: TabState = .statuses(statusesState: .loading) {
var accountState: AccountState = .loading
var tabState: TabState = .statuses(statusesState: .loading) {
didSet {
/// Forward viewModel tabState related to statusesState to statusesState property
/// for `StatusesFetcher` conformance as we wrap StatusesState in TabState
@ -70,18 +71,18 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
}
}
@Published var statusesState: StatusesState = .loading
var statusesState: StatusesState = .loading
@Published var relationship: Relationship?
@Published var pinned: [Status] = []
@Published var favorites: [Status] = []
@Published var bookmarks: [Status] = []
var relationship: Relationship?
var pinned: [Status] = []
var favorites: [Status] = []
var bookmarks: [Status] = []
private var favoritesNextPage: LinkHandler?
private var bookmarksNextPage: LinkHandler?
@Published var featuredTags: [FeaturedTag] = []
@Published var fields: [Account.Field] = []
@Published var familiarFollowers: [Account] = []
@Published var selectedTab = Tab.statuses {
var featuredTags: [FeaturedTag] = []
var fields: [Account.Field] = []
var familiarFollowers: [Account] = []
var selectedTab = Tab.statuses {
didSet {
switch selectedTab {
case .statuses, .postsAndReplies, .media:
@ -95,8 +96,8 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
}
}
@Published var translation: Translation?
@Published var isLoadingTranslation = false
var translation: Translation?
var isLoadingTranslation = false
private(set) var account: Account?
private var tabTask: Task<Void, Never>?

View file

@ -4,14 +4,15 @@ import EmojiText
import Env
import Models
import Network
import Observation
import SwiftUI
@MainActor
public class AccountsListRowViewModel: ObservableObject {
@Observable public class AccountsListRowViewModel {
var client: Client?
@Published var account: Account
@Published var relationShip: Relationship?
var account: Account
var relationShip: Relationship?
public init(account: Account, relationShip: Relationship? = nil) {
self.account = account
@ -21,11 +22,11 @@ public class AccountsListRowViewModel: ObservableObject {
public struct AccountsListRow: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var client: Client
@Environment(CurrentAccount.self) private var currentAccount
@Environment(RouterPath.self) private var routerPath
@Environment(Client.self) private var client
@StateObject var viewModel: AccountsListRowViewModel
@State var viewModel: AccountsListRowViewModel
@State private var isEditingRelationshipNote: Bool = false
@ -33,7 +34,7 @@ public struct AccountsListRow: View {
let requestUpdated: (() -> Void)?
public init(viewModel: AccountsListRowViewModel, isFollowRequest: Bool = false, requestUpdated: (() -> Void)? = nil) {
_viewModel = StateObject(wrappedValue: viewModel)
self.viewModel = viewModel
self.isFollowRequest = isFollowRequest
self.requestUpdated = requestUpdated
}
@ -117,8 +118,8 @@ public struct AccountsListRow: View {
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.environmentObject(theme)
.environmentObject(currentAccount)
.environmentObject(client)
.environment(currentAccount)
.environment(client)
}
}
}

View file

@ -7,13 +7,13 @@ import SwiftUI
public struct AccountsListView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@EnvironmentObject private var currentAccount: CurrentAccount
@StateObject private var viewModel: AccountsListViewModel
@Environment(Client.self) private var client
@Environment(CurrentAccount.self) private var currentAccount
@State private var viewModel: AccountsListViewModel
@State private var didAppear: Bool = false
public init(mode: AccountsListMode) {
_viewModel = StateObject(wrappedValue: .init(mode: mode))
_viewModel = .init(initialValue: .init(mode: mode))
}
public var body: some View {

View file

@ -1,5 +1,6 @@
import Models
import Network
import Observation
import SwiftUI
public enum AccountsListMode {
@ -24,7 +25,7 @@ public enum AccountsListMode {
}
@MainActor
class AccountsListViewModel: ObservableObject {
@Observable class AccountsListViewModel {
var client: Client?
let mode: AccountsListMode
@ -44,7 +45,7 @@ class AccountsListViewModel: ObservableObject {
private var accounts: [Account] = []
private var relationships: [Relationship] = []
@Published var state = State.loading
var state = State.loading
private var nextPageId: String?

View file

@ -5,10 +5,10 @@ import SwiftUI
public struct EditAccountView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var theme: Theme
@StateObject private var viewModel = EditAccountViewModel()
@State private var viewModel = EditAccountViewModel()
public init() {}

View file

@ -1,13 +1,14 @@
import Models
import Network
import Observation
import SwiftUI
@MainActor
class EditAccountViewModel: ObservableObject {
class FieldEditViewModel: ObservableObject, Identifiable {
@Observable class EditAccountViewModel {
@Observable class FieldEditViewModel: Identifiable {
let id = UUID().uuidString
@Published var name: String = ""
@Published var value: String = ""
var name: String = ""
var value: String = ""
init(name: String, value: String) {
self.name = name
@ -17,18 +18,18 @@ class EditAccountViewModel: ObservableObject {
public var client: Client?
@Published var displayName: String = ""
@Published var note: String = ""
@Published var postPrivacy = Models.Visibility.pub
@Published var isSensitive: Bool = false
@Published var isBot: Bool = false
@Published var isLocked: Bool = false
@Published var isDiscoverable: Bool = false
@Published var fields: [FieldEditViewModel] = []
var displayName: String = ""
var note: String = ""
var postPrivacy = Models.Visibility.pub
var isSensitive: Bool = false
var isBot: Bool = false
var isLocked: Bool = false
var isDiscoverable: Bool = false
var fields: [FieldEditViewModel] = []
@Published var isLoading: Bool = true
@Published var isSaving: Bool = false
@Published var saveError: Bool = false
var isLoading: Bool = true
var isSaving: Bool = false
var saveError: Bool = false
init() {}

View file

@ -5,12 +5,10 @@ import SwiftUI
public struct EditRelationshipNoteView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
// need this model to refresh after storing the new note on mastodon
var accountDetailViewModel: AccountDetailViewModel
@StateObject private var viewModel = EditRelationshipNoteViewModel()
@State var accountDetailViewModel: AccountDetailViewModel
@State private var viewModel = EditRelationshipNoteViewModel()
public var body: some View {
NavigationStack {

View file

@ -1,14 +1,15 @@
import Network
import Observation
import SwiftUI
@MainActor
class EditRelationshipNoteViewModel: ObservableObject {
@Observable class EditRelationshipNoteViewModel {
public var note: String = ""
public var relatedAccountId: String?
public var client: Client?
@Published var isSaving: Bool = false
@Published var saveError: Bool = false
var isSaving: Bool = false
var saveError: Bool = false
init() {}
@ -18,7 +19,7 @@ class EditRelationshipNoteViewModel: ObservableObject {
{
isSaving = true
do {
let _ = try await client!.post(endpoint: Accounts.relationshipNote(id: relatedAccountId!, json: RelationshipNoteData(note: note)))
_ = try await client!.post(endpoint: Accounts.relationshipNote(id: relatedAccountId!, json: RelationshipNoteData(note: note)))
} catch {
isSaving = false
saveError = true

View file

@ -8,8 +8,8 @@ struct EditFilterView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var account: CurrentAccount
@EnvironmentObject private var client: Client
@Environment(CurrentAccount.self) private var account
@Environment(Client.self) private var client
@State private var isSavingFilter: Bool = false
@State private var filter: ServerFilter?
@ -91,9 +91,9 @@ struct EditFilterView: View {
Text(duration.description).tag(duration)
}
}
.onChange(of: expirySelection) { duration in
if duration != .custom {
expiresAt = Date(timeIntervalSinceNow: TimeInterval(duration.rawValue))
.onChange(of: expirySelection) { _, newValue in
if newValue != .custom {
expiresAt = Date(timeIntervalSinceNow: TimeInterval(newValue.rawValue))
}
}
if expirySelection != .infinite {
@ -227,7 +227,7 @@ struct EditFilterView: View {
} label: {
EmptyView()
}
.onChange(of: filterAction) { _ in
.onChange(of: filterAction) {
Task {
await saveFilter()
}

View file

@ -8,8 +8,8 @@ public struct FiltersListView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var account: CurrentAccount
@EnvironmentObject private var client: Client
@Environment(CurrentAccount.self) private var account
@Environment(Client.self) private var client
@State private var isLoading: Bool = true
@State private var filters: [ServerFilter] = []

View file

@ -2,17 +2,18 @@ import Combine
import Foundation
import Models
import Network
import Observation
import SwiftUI
@MainActor
public class FollowButtonViewModel: ObservableObject {
@Observable public class FollowButtonViewModel {
var client: Client?
public let accountId: String
public let shouldDisplayNotify: Bool
public let relationshipUpdated: (Relationship) -> Void
@Published public private(set) var relationship: Relationship
@Published public private(set) var isUpdating: Bool = false
public private(set) var relationship: Relationship
public private(set) var isUpdating: Bool = false
public init(accountId: String,
relationship: Relationship,
@ -75,11 +76,11 @@ public class FollowButtonViewModel: ObservableObject {
}
public struct FollowButton: View {
@EnvironmentObject private var client: Client
@StateObject private var viewModel: FollowButtonViewModel
@Environment(Client.self) private var client
@State private var viewModel: FollowButtonViewModel
public init(viewModel: FollowButtonViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
_viewModel = .init(initialValue: viewModel)
}
public var body: some View {

View file

@ -7,7 +7,7 @@ let package = Package(
name: "AppAccount",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -5,14 +5,14 @@ import SwiftUI
public struct AppAccountView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var appAccounts: AppAccountsManager
@Environment(RouterPath.self) private var routerPath
@Environment(AppAccountsManager.self) private var appAccounts
@EnvironmentObject private var preferences: UserPreferences
@StateObject var viewModel: AppAccountViewModel
@State var viewModel: AppAccountViewModel
public init(viewModel: AppAccountViewModel) {
_viewModel = .init(wrappedValue: viewModel)
self.viewModel = viewModel
}
public var body: some View {

View file

@ -2,10 +2,11 @@ import Combine
import DesignSystem
import Models
import Network
import Observation
import SwiftUI
@MainActor
public class AppAccountViewModel: ObservableObject {
@Observable public class AppAccountViewModel {
private static var avatarsCache: [String: UIImage] = [:]
private static var accountsCache: [String: Account] = [:]
@ -15,7 +16,7 @@ public class AppAccountViewModel: ObservableObject {
let isInNavigation: Bool
let showBadge: Bool
@Published var account: Account? {
var account: Account? {
didSet {
if let account {
refreshAcct(account: account)

View file

@ -2,14 +2,15 @@ import Combine
import Env
import Models
import Network
import Observation
import SwiftUI
@MainActor
public class AppAccountsManager: ObservableObject {
@Observable public class AppAccountsManager {
@AppStorage("latestCurrentAccountKey", store: UserPreferences.sharedDefault)
public static var latestCurrentAccountKey: String = ""
@Published public var currentAccount: AppAccount {
public var currentAccount: AppAccount {
didSet {
Self.latestCurrentAccountKey = currentAccount.id
currentClient = .init(server: currentAccount.server,
@ -17,8 +18,8 @@ public class AppAccountsManager: ObservableObject {
}
}
@Published public var availableAccounts: [AppAccount]
@Published public var currentClient: Client
public var availableAccounts: [AppAccount]
public var currentClient: Client
public var pushAccounts: [PushAccount] {
availableAccounts.filter { $0.oauthToken != nil }

View file

@ -4,11 +4,11 @@ import SwiftUI
public struct AppAccountsSelectorView: View {
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var appAccounts: AppAccountsManager
@Environment(CurrentAccount.self) private var currentAccount
@Environment(AppAccountsManager.self) private var appAccounts
@EnvironmentObject private var theme: Theme
@ObservedObject var routerPath: RouterPath
var routerPath: RouterPath
@State private var accountsViewModel: [AppAccountViewModel] = []
@State private var isPresented: Bool = false
@ -61,7 +61,7 @@ public struct AppAccountsSelectorView: View {
}
}
})
.onChange(of: currentAccount.account?.id) { _ in
.onChange(of: currentAccount.account?.id) {
refreshAccounts()
}
.onAppear {

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Conversations",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -10,14 +10,14 @@ public struct ConversationDetailView: View {
static let bottomAnchor = "bottom"
}
@EnvironmentObject private var quickLook: QuickLook
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var client: Client
@Environment(QuickLook.self) private var quickLook
@Environment(RouterPath.self) private var routerPath
@Environment(CurrentAccount.self) private var currentAccount
@Environment(Client.self) private var client
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var watcher: StreamWatcher
@Environment(StreamWatcher.self) private var watcher
@StateObject private var viewModel: ConversationDetailViewModel
@State private var viewModel: ConversationDetailViewModel
@FocusState private var isMessageFieldFocused: Bool
@ -25,7 +25,7 @@ public struct ConversationDetailView: View {
@State private var didAppear: Bool = false
public init(conversation: Conversation) {
_viewModel = StateObject(wrappedValue: .init(conversation: conversation))
_viewModel = .init(initialValue: .init(conversation: conversation))
}
public var body: some View {
@ -85,7 +85,7 @@ public struct ConversationDetailView: View {
}
}
}
.onChange(of: watcher.latestEvent?.id) { _ in
.onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent {
viewModel.handleEvent(event: latestEvent)
DispatchQueue.main.async {

View file

@ -4,16 +4,16 @@ import Network
import SwiftUI
@MainActor
class ConversationDetailViewModel: ObservableObject {
@Observable class ConversationDetailViewModel {
var client: Client?
var conversation: Conversation
@Published var isLoadingMessages: Bool = true
@Published var messages: [Status] = []
var isLoadingMessages: Bool = true
var messages: [Status] = []
@Published var isSendingMessage: Bool = false
@Published var newMessageText: String = ""
var isSendingMessage: Bool = false
var newMessageText: String = ""
init(conversation: Conversation) {
self.conversation = conversation

View file

@ -6,10 +6,10 @@ import NukeUI
import SwiftUI
struct ConversationMessageView: View {
@EnvironmentObject private var quickLook: QuickLook
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var client: Client
@Environment(QuickLook.self) private var quickLook
@Environment(RouterPath.self) private var routerPath
@Environment(CurrentAccount.self) private var currentAccount
@Environment(Client.self) private var client
@EnvironmentObject private var theme: Theme
let message: Status

View file

@ -6,13 +6,13 @@ import Network
import SwiftUI
struct ConversationsListRow: View {
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentAccount: CurrentAccount
@Environment(CurrentAccount.self) private var currentAccount
@Binding var conversation: Conversation
@ObservedObject var viewModel: ConversationsListViewModel
var viewModel: ConversationsListViewModel
var body: some View {
Button {

View file

@ -7,12 +7,12 @@ import SwiftUI
public struct ConversationsListView: View {
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var client: Client
@Environment(RouterPath.self) private var routerPath
@Environment(StreamWatcher.self) private var watcher
@Environment(Client.self) private var client
@EnvironmentObject private var theme: Theme
@StateObject private var viewModel = ConversationsListViewModel()
@State private var viewModel = ConversationsListViewModel()
public init() {}
@ -83,7 +83,7 @@ public struct ConversationsListView: View {
SecondaryColumnToolbarItem()
}
}
.onChange(of: watcher.latestEvent?.id) { _ in
.onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent {
viewModel.handleEvent(event: latestEvent)
}

View file

@ -3,13 +3,13 @@ import Network
import SwiftUI
@MainActor
class ConversationsListViewModel: ObservableObject {
@Observable class ConversationsListViewModel {
var client: Client?
@Published var isLoadingFirstPage: Bool = true
@Published var isLoadingNextPage: Bool = false
@Published var conversations: [Conversation] = []
@Published var isError: Bool = false
var isLoadingFirstPage: Bool = true
var isLoadingNextPage: Bool = false
var conversations: [Conversation] = []
var isError: Bool = false
var nextPage: LinkHandler?

View file

@ -7,7 +7,7 @@ let package = Package(
name: "DesignSystem",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -1,7 +1,7 @@
import Combine
import UIKit
public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
@Observable public class SceneDelegate: NSObject, UIWindowSceneDelegate {
public var window: UIWindow?
public var windowWidth: CGFloat {

View file

@ -41,16 +41,16 @@ struct ThemeApplier: ViewModifier {
setWindowUserInterfaceStyle(from: theme.selectedScheme)
setBarsColor(theme.primaryBackgroundColor)
}
.onChange(of: theme.tintColor) { newValue in
.onChange(of: theme.tintColor) { _, newValue in
setWindowTint(newValue)
}
.onChange(of: theme.primaryBackgroundColor) { newValue in
.onChange(of: theme.primaryBackgroundColor) { _, newValue in
setBarsColor(newValue)
}
.onChange(of: theme.selectedScheme) { newValue in
.onChange(of: theme.selectedScheme) { _, newValue in
setWindowUserInterfaceStyle(from: newValue)
}
.onChange(of: colorScheme) { newColorScheme in
.onChange(of: colorScheme) { _, newColorScheme in
if theme.followSystemColorScheme,
let sets = availableColorsSets
.first(where: { $0.light.name == theme.selectedSet || $0.dark.name == theme.selectedSet })

View file

@ -3,6 +3,7 @@ import NukeUI
import Shimmer
import SwiftUI
@MainActor
public struct AvatarView: View {
@Environment(\.redactionReasons) private var reasons
@EnvironmentObject private var theme: Theme

View file

@ -3,7 +3,7 @@ import Models
import SwiftUI
public struct FollowRequestButtons: View {
@EnvironmentObject private var currentAccount: CurrentAccount
@Environment(CurrentAccount.self) private var currentAccount
let account: Account
let requestUpdated: (() -> Void)?

View file

@ -24,7 +24,7 @@ public extension View {
@MainActor
public struct StatusEditorToolbarItem: ToolbarContent {
@EnvironmentObject private var routerPath: RouterPath
@Environment(RouterPath.self) private var routerPath
let visibility: Models.Visibility

View file

@ -3,7 +3,7 @@ import Models
import SwiftUI
public struct TagRowView: View {
@EnvironmentObject private var routerPath: RouterPath
@Environment(RouterPath.self) private var routerPath
let tag: Tag

View file

@ -81,7 +81,7 @@ struct ThemeBoxView: View {
.onAppear {
isSelected = theme.selectedSet.rawValue == color.name.rawValue
}
.onChange(of: theme.selectedSet) { newValue in
.onChange(of: theme.selectedSet) { _, newValue in
isSelected = newValue.rawValue == color.name.rawValue
}
.onTapGesture {

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Env",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -2,18 +2,19 @@ import Combine
import Foundation
import Models
import Network
import Observation
@MainActor
public class CurrentAccount: ObservableObject {
@Observable public class CurrentAccount {
private static var accountsCache: [String: Account] = [:]
@Published public private(set) var account: Account?
@Published public private(set) var lists: [List] = []
@Published public private(set) var tags: [Tag] = []
@Published public private(set) var followRequests: [Account] = []
@Published public private(set) var isUpdating: Bool = false
@Published public private(set) var updatingFollowRequestAccountIds = Set<String>()
@Published public private(set) var isLoadingAccount: Bool = false
public private(set) var account: Account?
public private(set) var lists: [List] = []
public private(set) var tags: [Tag] = []
public private(set) var followRequests: [Account] = []
public private(set) var isUpdating: Bool = false
public private(set) var updatingFollowRequestAccountIds = Set<String>()
public private(set) var isLoadingAccount: Bool = false
private var client: Client?

View file

@ -2,10 +2,11 @@ import Combine
import Foundation
import Models
import Network
import Observation
@MainActor
public class CurrentInstance: ObservableObject {
@Published public private(set) var instance: Instance?
@Observable public class CurrentInstance {
public private(set) var instance: Instance?
private var client: Client?

View file

@ -21,11 +21,11 @@ private struct IsSupporter: EnvironmentKey {
static let defaultValue: Bool = false
}
private struct IsStatusDetailLoaded: EnvironmentKey {
private struct IsStatusFocused: EnvironmentKey {
static let defaultValue: Bool = false
}
private struct IsStatusFocused: EnvironmentKey {
private struct IsStatusReplyToPrevious: EnvironmentKey {
static let defaultValue: Bool = false
}
@ -55,13 +55,13 @@ public extension EnvironmentValues {
set { self[IsSupporter.self] = newValue }
}
var isStatusDetailLoaded: Bool {
get { self[IsStatusDetailLoaded.self] }
set { self[IsStatusDetailLoaded.self] = newValue }
}
var isStatusFocused: Bool {
get { self[IsStatusFocused.self] }
set { self[IsStatusFocused.self] = newValue }
}
var isStatusReplyToPrevious: Bool {
get { self[IsStatusReplyToPrevious.self] }
set { self[IsStatusReplyToPrevious.self] = newValue }
}
}

View file

@ -4,6 +4,7 @@ import Foundation
import KeychainSwift
import Models
import Network
import Observation
import SwiftUI
import UserNotifications
@ -28,7 +29,7 @@ public struct HandledNotification: Equatable {
}
@MainActor
public class PushNotificationsService: NSObject, ObservableObject {
@Observable public class PushNotificationsService: NSObject {
enum Constants {
static let endpoint = "https://icecubesrelay.fly.dev"
static let keychainAuthKey = "notifications_auth_key"
@ -39,9 +40,9 @@ public class PushNotificationsService: NSObject, ObservableObject {
public private(set) var subscriptions: [PushNotificationSubscriptionSettings] = []
@Published public var pushToken: Data?
public var pushToken: Data?
@Published public var handledNotification: HandledNotification?
public var handledNotification: HandledNotification?
override init() {
super.init()
@ -162,14 +163,14 @@ extension Data {
}
@MainActor
public class PushNotificationSubscriptionSettings: ObservableObject {
@Published public var isEnabled: Bool = true
@Published public var isFollowNotificationEnabled: Bool = true
@Published public var isFavoriteNotificationEnabled: Bool = true
@Published public var isReblogNotificationEnabled: Bool = true
@Published public var isMentionNotificationEnabled: Bool = true
@Published public var isPollNotificationEnabled: Bool = true
@Published public var isNewPostsNotificationEnabled: Bool = true
@Observable public class PushNotificationSubscriptionSettings {
public var isEnabled: Bool = true
public var isFollowNotificationEnabled: Bool = true
public var isFavoriteNotificationEnabled: Bool = true
public var isReblogNotificationEnabled: Bool = true
public var isMentionNotificationEnabled: Bool = true
public var isPollNotificationEnabled: Bool = true
public var isNewPostsNotificationEnabled: Bool = true
public let account: PushAccount

View file

@ -3,8 +3,8 @@ import Combine
import SwiftUI
@MainActor
public class QuickLook: ObservableObject {
@Published public var url: URL? {
@Observable public class QuickLook {
public var url: URL? {
didSet {
if url == nil {
cleanup(urls: urls)
@ -12,9 +12,9 @@ public class QuickLook: ObservableObject {
}
}
@Published public private(set) var urls: [URL] = []
@Published public private(set) var isPreparing: Bool = false
@Published public private(set) var latestError: Error?
public private(set) var urls: [URL] = []
public private(set) var isPreparing: Bool = false
public private(set) var latestError: Error?
public init() {}

View file

@ -2,6 +2,7 @@ import Combine
import Foundation
import Models
import Network
import Observation
import SwiftUI
public enum RouterDestination: Hashable {
@ -69,12 +70,12 @@ public enum SheetDestination: Identifiable {
}
@MainActor
public class RouterPath: ObservableObject {
@Observable public class RouterPath {
public var client: Client?
public var urlHandler: ((URL) -> OpenURLAction.Result)?
@Published public var path: [RouterDestination] = []
@Published public var presentedSheet: SheetDestination?
public var path: [RouterDestination] = []
public var presentedSheet: SheetDestination?
public init() {}

View file

@ -1,10 +1,11 @@
import Foundation
import Models
import Network
import Observation
import SwiftUI
@MainActor
public protocol StatusDataControlling: ObservableObject {
public protocol StatusDataControlling {
var isReblogged: Bool { get set }
var isBookmarked: Bool { get set }
var isFavorited: Bool { get set }
@ -43,13 +44,13 @@ public final class StatusDataControllerProvider {
for status in statuses {
let realStatus: AnyStatus = status.reblog ?? status
let controller = dataController(for: realStatus, client: client)
controller.updateFrom(status: realStatus, publishUpdate: false)
controller.updateFrom(status: realStatus)
}
}
}
@MainActor
public final class StatusDataController: StatusDataControlling {
@Observable public final class StatusDataController: StatusDataControlling {
private let status: AnyStatus
private let client: Client
@ -74,7 +75,7 @@ public final class StatusDataController: StatusDataControlling {
favoritesCount = status.favouritesCount
}
public func updateFrom(status: AnyStatus, publishUpdate: Bool) {
public func updateFrom(status: AnyStatus) {
isReblogged = status.reblogged == true
isBookmarked = status.bookmarked == true
isFavorited = status.favourited == true
@ -82,10 +83,6 @@ public final class StatusDataController: StatusDataControlling {
reblogsCount = status.reblogsCount
repliesCount = status.repliesCount
favoritesCount = status.favouritesCount
if publishUpdate {
objectWillChange.send()
}
}
public func toggleFavorite(remoteStatus: String?) async {
@ -94,14 +91,12 @@ public final class StatusDataController: StatusDataControlling {
let id = remoteStatus ?? status.id
let endpoint = isFavorited ? Statuses.favorite(id: id) : Statuses.unfavorite(id: id)
favoritesCount += isFavorited ? 1 : -1
objectWillChange.send()
do {
let status: Status = try await client.post(endpoint: endpoint)
updateFrom(status: status, publishUpdate: true)
updateFrom(status: status)
} catch {
isFavorited.toggle()
favoritesCount += isFavorited ? -1 : 1
objectWillChange.send()
}
}
@ -111,14 +106,12 @@ public final class StatusDataController: StatusDataControlling {
let id = remoteStatus ?? status.id
let endpoint = isReblogged ? Statuses.reblog(id: id) : Statuses.unreblog(id: id)
reblogsCount += isReblogged ? 1 : -1
objectWillChange.send()
do {
let status: Status = try await client.post(endpoint: endpoint)
updateFrom(status: status.reblog ?? status, publishUpdate: true)
updateFrom(status: status.reblog ?? status)
} catch {
isReblogged.toggle()
reblogsCount += isReblogged ? -1 : 1
objectWillChange.send()
}
}
@ -127,13 +120,11 @@ public final class StatusDataController: StatusDataControlling {
isBookmarked.toggle()
let id = remoteStatus ?? status.id
let endpoint = isBookmarked ? Statuses.bookmark(id: id) : Statuses.unbookmark(id: id)
objectWillChange.send()
do {
let status: Status = try await client.post(endpoint: endpoint)
updateFrom(status: status, publishUpdate: true)
updateFrom(status: status)
} catch {
isBookmarked.toggle()
objectWillChange.send()
}
}
}

View file

@ -2,9 +2,10 @@ import Combine
import Foundation
import Models
import Network
import Observation
@MainActor
public class StreamWatcher: ObservableObject {
@Observable public class StreamWatcher {
private var client: Client?
private var task: URLSessionWebSocketTask?
private var watchedStreams: [Stream] = []
@ -21,9 +22,9 @@ public class StreamWatcher: ObservableObject {
case direct
}
@Published public var events: [any StreamEvent] = []
@Published public var unreadNotificationsCount: Int = 0
@Published public var latestEvent: (any StreamEvent)?
public var events: [any StreamEvent] = []
public var unreadNotificationsCount: Int = 0
public var latestEvent: (any StreamEvent)?
public init() {
decoder.keyDecodingStrategy = .convertFromSnakeCase

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Explore",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -9,10 +9,10 @@ import SwiftUI
public struct ExploreView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@StateObject private var viewModel = ExploreViewModel()
@State private var viewModel = ExploreViewModel()
public init() {}
@ -89,6 +89,12 @@ public struct ExploreView: View {
Text(scope.localizedString)
}
}
.task(id: viewModel.searchQuery) {
do {
try await Task.sleep(for: .milliseconds(150))
await viewModel.search()
} catch {}
}
}
private var quickAccessView: some View {
@ -117,7 +123,7 @@ public struct ExploreView: View {
private var loadingView: some View {
ForEach(Status.placeholders()) { status in
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.padding(.vertical, 8)
.redacted(reason: .placeholder)
.listRowBackground(theme.primaryBackgroundColor)
@ -148,7 +154,7 @@ public struct ExploreView: View {
if !results.statuses.isEmpty, viewModel.searchScope == .all || viewModel.searchScope == .posts {
Section("explore.section.posts") {
ForEach(results.statuses) { status in
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.listRowBackground(theme.primaryBackgroundColor)
.padding(.vertical, 8)
}
@ -196,7 +202,7 @@ public struct ExploreView: View {
ForEach(viewModel.trendingStatuses
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count))
{ status in
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.listRowBackground(theme.primaryBackgroundColor)
.padding(.vertical, 8)
}

View file

@ -1,10 +1,10 @@
import Combine
import Models
import Network
import Observation
import SwiftUI
@MainActor
class ExploreViewModel: ObservableObject {
@Observable class ExploreViewModel {
enum SearchScope: String, CaseIterable {
case all, people, hashtags, posts
@ -39,34 +39,23 @@ class ExploreViewModel: ObservableObject {
trendingLinks.isEmpty && trendingTags.isEmpty && trendingStatuses.isEmpty && suggestedAccounts.isEmpty
}
@Published var searchQuery = "" {
var searchQuery = "" {
didSet {
isSearching = true
}
}
@Published var results: [String: SearchResults] = [:]
@Published var isLoaded = false
@Published var isSearching = false
@Published var suggestedAccounts: [Account] = []
@Published var suggestedAccountsRelationShips: [Relationship] = []
@Published var trendingTags: [Tag] = []
@Published var trendingStatuses: [Status] = []
@Published var trendingLinks: [Card] = []
@Published var searchScope: SearchScope = .all
var results: [String: SearchResults] = [:]
var isLoaded = false
var isSearching = false
var suggestedAccounts: [Account] = []
var suggestedAccountsRelationShips: [Relationship] = []
var trendingTags: [Tag] = []
var trendingStatuses: [Status] = []
var trendingLinks: [Card] = []
var searchScope: SearchScope = .all
private var searchTask: Task<Void, Never>?
private var cancellables = Set<AnyCancellable>()
init() {
$searchQuery
.removeDuplicates()
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main)
.sink(receiveValue: { [weak self] _ in
self?.search()
})
.store(in: &cancellables)
}
init() {}
func fetchTrending() async {
guard let client else { return }
@ -104,29 +93,24 @@ class ExploreViewModel: ObservableObject {
trendingLinks: trendingLinks)
}
func search() {
guard !searchQuery.isEmpty else { return }
isSearching = true
searchTask?.cancel()
searchTask = nil
searchTask = Task {
guard let client else { return }
do {
var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery,
type: nil,
offset: nil,
following: nil),
forceVersion: .v2)
let relationships: [Relationship] =
try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map(\.id)))
results.relationships = relationships
withAnimation {
self.results[searchQuery] = results
isSearching = false
}
} catch {
func search() async {
guard let client else { return }
do {
try await Task.sleep(for: .milliseconds(250))
var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery,
type: nil,
offset: nil,
following: nil),
forceVersion: .v2)
let relationships: [Relationship] =
try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map(\.id)))
results.relationships = relationships
withAnimation {
self.results[searchQuery] = results
isSearching = false
}
} catch {
isSearching = false
}
}
}

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Lists",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -6,16 +6,16 @@ import SwiftUI
public struct ListAddAccountView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentAccount: CurrentAccount
@StateObject private var viewModel: ListAddAccountViewModel
@Environment(CurrentAccount.self) private var currentAccount
@State private var viewModel: ListAddAccountViewModel
@State private var isCreateListAlertPresented: Bool = false
@State private var createListTitle: String = ""
public init(account: Account) {
_viewModel = StateObject(wrappedValue: .init(account: account))
_viewModel = .init(initialValue: .init(account: account))
}
public var body: some View {

View file

@ -1,13 +1,14 @@
import Models
import Network
import Observation
import SwiftUI
@MainActor
class ListAddAccountViewModel: ObservableObject {
@Observable class ListAddAccountViewModel {
let account: Account
@Published var inLists: [Models.List] = []
@Published var isLoadingInfo: Bool = true
var inLists: [Models.List] = []
var isLoadingInfo: Bool = true
var client: Client?

View file

@ -7,12 +7,12 @@ import SwiftUI
public struct ListEditView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@StateObject private var viewModel: ListEditViewModel
@State private var viewModel: ListEditViewModel
public init(list: Models.List) {
_viewModel = StateObject(wrappedValue: .init(list: list))
_viewModel = .init(initialValue: .init(list: list))
}
public var body: some View {

View file

@ -1,16 +1,17 @@
import Combine
import Models
import Network
import Observation
import SwiftUI
@MainActor
public class ListEditViewModel: ObservableObject {
@Observable public class ListEditViewModel {
let list: Models.List
var client: Client?
@Published var isLoadingAccounts: Bool = true
@Published var accounts: [Account] = []
var isLoadingAccounts: Bool = true
var accounts: [Account] = []
init(list: Models.List) {
self.list = list

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Models",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Network",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -1,10 +1,11 @@
import Combine
import Foundation
import Models
import Observation
import os
import SwiftUI
public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
@Observable public final class Client: Equatable, Identifiable, Hashable {
public static func == (lhs: Client, rhs: Client) -> Bool {
let lhsToken = lhs.critical.withLock { $0.oauthToken }
let rhsToken = rhs.critical.withLock { $0.oauthToken }

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Notifications",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -149,16 +149,16 @@ struct NotificationRowView: View {
if let status = notification.status {
HStack {
if type == .mention {
StatusRowView(viewModel: { .init(status: status,
client: client,
routerPath: routerPath,
showActions: true) })
StatusRowView(viewModel: .init(status: status,
client: client,
routerPath: routerPath,
showActions: true))
} else {
StatusRowView(viewModel: { .init(status: status,
client: client,
routerPath: routerPath,
showActions: false,
textDisabled: true) })
StatusRowView(viewModel: .init(status: status,
client: client,
routerPath: routerPath,
showActions: false,
textDisabled: true))
.lineLimit(4)
}
Spacer()

View file

@ -8,11 +8,11 @@ import SwiftUI
public struct NotificationsListView: View {
@Environment(\.scenePhase) private var scenePhase
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var account: CurrentAccount
@StateObject private var viewModel = NotificationsViewModel()
@Environment(StreamWatcher.self) private var watcher
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@Environment(CurrentAccount.self) private var account
@State private var viewModel = NotificationsViewModel()
let lockedType: Models.Notification.NotificationType?
@ -88,13 +88,13 @@ public struct NotificationsListView: View {
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
SoundEffectManager.shared.playSound(of: .refresh)
}
.onChange(of: watcher.latestEvent?.id, perform: { _ in
.onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent {
viewModel.handleEvent(event: latestEvent)
}
})
.onChange(of: scenePhase, perform: { scenePhase in
switch scenePhase {
}
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:
Task {
await viewModel.fetchNotifications()
@ -102,7 +102,7 @@ public struct NotificationsListView: View {
default:
break
}
})
}
}
@ViewBuilder

View file

@ -2,10 +2,11 @@ import Env
import Foundation
import Models
import Network
import Observation
import SwiftUI
@MainActor
class NotificationsViewModel: ObservableObject {
@Observable class NotificationsViewModel {
public enum State {
public enum PagingState {
case none, hasNextPage, loadingNextPage
@ -35,8 +36,8 @@ class NotificationsViewModel: ObservableObject {
var currentAccount: CurrentAccount?
@Published var state: State = .loading
@Published var selectedType: Models.Notification.NotificationType? {
var state: State = .loading
var selectedType: Models.Notification.NotificationType? {
didSet {
if oldValue != selectedType {
consolidatedNotifications = []

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Status",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -7,12 +7,12 @@ import SwiftUI
public struct StatusDetailView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@Environment(CurrentAccount.self) private var currentAccount
@Environment(StreamWatcher.self) private var watcher
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@StateObject private var viewModel: StatusDetailViewModel
@State private var viewModel: StatusDetailViewModel
@State private var isLoaded: Bool = false
@State private var statusHeight: CGFloat = 0
@ -22,15 +22,15 @@ public struct StatusDetailView: View {
@AccessibilityFocusState private var initialFocusBugWorkaround: Bool
public init(statusId: String) {
_viewModel = StateObject(wrappedValue: { .init(statusId: statusId) }())
_viewModel = .init(wrappedValue: .init(statusId: statusId))
}
public init(status: Status) {
_viewModel = StateObject(wrappedValue: { .init(status: status) }())
_viewModel = .init(wrappedValue: .init(status: status))
}
public init(remoteStatusURL: URL) {
_viewModel = StateObject(wrappedValue: { .init(remoteStatusURL: remoteStatusURL) }())
_viewModel = .init(wrappedValue: .init(remoteStatusURL: remoteStatusURL))
}
public var body: some View {
@ -45,9 +45,8 @@ public struct StatusDetailView: View {
case .loading:
loadingDetailView
case let .display(statuses, date):
makeStatusesListView(statuses: statuses, date: date)
.id(date)
case let .display(statuses):
makeStatusesListView(statuses: statuses)
if !isLoaded {
loadingContextView
@ -69,12 +68,12 @@ public struct StatusDetailView: View {
.listStyle(.plain)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.onChange(of: viewModel.scrollToId, perform: { scrollToId in
if let scrollToId {
.onChange(of: viewModel.scrollToId) { _, newValue in
if let newValue {
viewModel.scrollToId = nil
proxy.scrollTo(scrollToId, anchor: .top)
proxy.scrollTo(newValue, anchor: .top)
}
})
}
.task {
guard !isLoaded else { return }
viewModel.client = client
@ -92,7 +91,7 @@ public struct StatusDetailView: View {
}
}
}
.onChange(of: watcher.latestEvent?.id) { _ in
.onChange(of: watcher.latestEvent?.id) {
guard let lastEvent = watcher.latestEvent else { return }
viewModel.handleEvent(event: lastEvent, currentAccount: currentAccount.account)
}
@ -101,69 +100,35 @@ public struct StatusDetailView: View {
.navigationBarTitleDisplayMode(.inline)
}
private func makeStatusesListView(statuses: [Status], date _: Date) -> some View {
private func makeStatusesListView(statuses: [Status]) -> some View {
ForEach(statuses) { status in
var isReplyToPrevious: Bool = false
if let index = statuses.firstIndex(where: { $0.id == status.id }),
index > 0,
statuses[index - 1].id == status.inReplyToId
{
if index == 1, statuses.count > 2 {
let nextStatus = statuses[2]
isReplyToPrevious = nextStatus.inReplyToId == status.id
} else if statuses.count == 2 {
isReplyToPrevious = false
} else {
isReplyToPrevious = true
}
}
let isReplyToPrevious = viewModel.isReplyToPreviousCache[status.id] ?? false
let viewModel: StatusRowViewModel = .init(status: status,
client: client,
routerPath: routerPath)
return HStack(spacing: 0) {
if isReplyToPrevious {
Rectangle()
.fill(theme.tintColor)
.frame(width: 2)
.accessibilityHidden(true)
Spacer(minLength: 8)
}
if self.viewModel.statusId == status.id {
makeCurrentStatusView(status: status)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
} else {
StatusRowView(viewModel: { viewModel })
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
}
}
.listRowBackground(viewModel.highlightRowColor)
.listRowInsets(.init(top: 12,
leading: .layoutPadding,
bottom: 12,
trailing: .layoutPadding))
}
}
let isFocused = self.viewModel.statusId == status.id
private func makeCurrentStatusView(status: Status) -> some View {
StatusRowView(viewModel: { .init(status: status,
client: client,
routerPath: routerPath) })
.environment(\.isStatusFocused, true)
.environment(\.isStatusDetailLoaded, !viewModel.isLoadingContext)
.accessibilityFocused($initialFocusBugWorkaround, equals: true)
.overlay {
GeometryReader { reader in
VStack {}
.onAppear {
statusHeight = reader.size.height
StatusRowView(viewModel: viewModel)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
.environment(\.isStatusReplyToPrevious, isReplyToPrevious)
.environment(\.isStatusFocused, isFocused)
.overlay {
if isFocused {
GeometryReader { reader in
VStack {}
.onAppear {
statusHeight = reader.size.height
}
}
}
}
}
.id(status.id)
// VoiceOver / Switch Control focus workaround
.onAppear {
initialFocusBugWorkaround = true
}
.id(status.id)
.listRowBackground(viewModel.highlightRowColor)
.listRowInsets(.init(top: 12,
leading: .layoutPadding,
bottom: 12,
trailing: .layoutPadding))
}
}
private var errorView: some View {
@ -181,7 +146,7 @@ public struct StatusDetailView: View {
private var loadingDetailView: some View {
ForEach(Status.placeholders()) { status in
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.redacted(reason: .placeholder)
}
}

View file

@ -5,7 +5,7 @@ import Network
import SwiftUI
@MainActor
class StatusDetailViewModel: ObservableObject {
@Observable class StatusDetailViewModel {
public var statusId: String?
public var remoteStatusURL: URL?
@ -13,13 +13,15 @@ class StatusDetailViewModel: ObservableObject {
var routerPath: RouterPath?
enum State {
case loading, display(statuses: [Status], date: Date), error(error: Error)
case loading, display(statuses: [Status]), error(error: Error)
}
@Published var state: State = .loading
@Published var isLoadingContext = true
@Published var title: LocalizedStringKey = ""
@Published var scrollToId: String?
var state: State = .loading
var title: LocalizedStringKey = ""
var scrollToId: String?
@ObservationIgnored
var isReplyToPreviousCache: [String: Bool] = [:]
init(statusId: String) {
state = .loading
@ -28,7 +30,7 @@ class StatusDetailViewModel: ObservableObject {
}
init(status: Status) {
state = .display(statuses: [status], date: Date())
state = .display(statuses: [status])
title = "status.post-from-\(status.account.displayNameWithoutEmojis)"
statusId = status.id
remoteStatusURL = nil
@ -74,23 +76,20 @@ class StatusDetailViewModel: ObservableObject {
private func fetchStatusDetail(animate: Bool) async {
guard let client, let statusId else { return }
do {
isLoadingContext = true
let data = try await fetchContextData(client: client, statusId: statusId)
title = "status.post-from-\(data.status.account.displayNameWithoutEmojis)"
var statuses = data.context.ancestors
statuses.append(data.status)
statuses.append(contentsOf: data.context.descendants)
cacheReplyTopPrevious(statuses: statuses)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
if animate {
withAnimation {
isLoadingContext = false
state = .display(statuses: statuses, date: Date())
state = .display(statuses: statuses)
}
} else {
isLoadingContext = false
state = .display(statuses: statuses, date: Date())
state = .display(statuses: statuses)
scrollToId = statusId
}
} catch {
@ -108,6 +107,27 @@ class StatusDetailViewModel: ObservableObject {
return try await .init(status: status, context: context)
}
private func cacheReplyTopPrevious(statuses: [Status]) {
isReplyToPreviousCache = [:]
for status in statuses {
var isReplyToPrevious: Bool = false
if let index = statuses.firstIndex(where: { $0.id == status.id }),
index > 0,
statuses[index - 1].id == status.inReplyToId
{
if index == 1, statuses.count > 2 {
let nextStatus = statuses[2]
isReplyToPrevious = nextStatus.inReplyToId == status.id
} else if statuses.count == 2 {
isReplyToPrevious = false
} else {
isReplyToPrevious = true
}
}
isReplyToPreviousCache[status.id] = isReplyToPrevious
}
}
func handleEvent(event: any StreamEvent, currentAccount: Account?) {
Task {
if let event = event as? StreamEventUpdate,

View file

@ -8,11 +8,11 @@ import SwiftUI
struct StatusEditorAccessoryView: View {
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentInstance: CurrentInstance
@Environment(CurrentInstance.self) private var currentInstance
@Environment(\.colorScheme) private var colorScheme
@FocusState<Bool>.Binding var isSpoilerTextFocused: Bool
@ObservedObject var viewModel: StatusEditorViewModel
var viewModel: StatusEditorViewModel
@State private var isDraftsSheetDisplayed: Bool = false
@State private var isLanguageSheetDisplayed: Bool = false
@ -24,6 +24,7 @@ struct StatusEditorAccessoryView: View {
@State private var isCameraPickerPresented: Bool = false
var body: some View {
@Bindable var viewModel = viewModel
VStack(spacing: 0) {
Divider()
HStack {

View file

@ -5,7 +5,7 @@ import SwiftUI
struct StatusEditorAutoCompleteView: View {
@EnvironmentObject private var theme: Theme
@ObservedObject var viewModel: StatusEditorViewModel
var viewModel: StatusEditorViewModel
var body: some View {
if !viewModel.mentionsSuggestions.isEmpty || !viewModel.tagsSuggestions.isEmpty {

View file

@ -7,8 +7,8 @@ import SwiftUI
struct StatusEditorMediaEditView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentInstance: CurrentInstance
@ObservedObject var viewModel: StatusEditorViewModel
@Environment(CurrentInstance.self) private var currentInstance
var viewModel: StatusEditorViewModel
let container: StatusEditorMediaContainer
@State private var imageDescription: String = ""

View file

@ -7,8 +7,8 @@ import SwiftUI
struct StatusEditorMediaView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentInstance: CurrentInstance
@ObservedObject var viewModel: StatusEditorViewModel
@Environment(CurrentInstance.self) private var currentInstance
var viewModel: StatusEditorViewModel
@State private var editingContainer: StatusEditorMediaContainer?
@State private var isErrorDisplayed: Bool = false

View file

@ -12,15 +12,15 @@ struct StatusEditorPollView: View {
@State private var currentFocusIndex: Int = 0
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentInstance: CurrentInstance
@Environment(CurrentInstance.self) private var currentInstance
@ObservedObject var viewModel: StatusEditorViewModel
var viewModel: StatusEditorViewModel
@Binding var showPoll: Bool
var body: some View {
@Bindable var viewModel = viewModel
let count = viewModel.pollOptions.count
VStack {
ForEach(0 ..< count, id: \.self) { index in
VStack {
@ -38,10 +38,6 @@ struct StatusEditorPollView: View {
addChoice(at: index)
}
}
.onChange(of: viewModel.pollOptions[index]) {
let maxCharacters: Int = currentInstance.instance?.configuration?.polls.maxCharactersPerOption ?? 50
viewModel.pollOptions[index] = String($0.prefix(maxCharacters))
}
if canAddMoreAt(index) {
Button {

View file

@ -12,22 +12,22 @@ import SwiftUI
import UIKit
public struct StatusEditorView: View {
@EnvironmentObject private var appAccounts: AppAccountsManager
@Environment(AppAccountsManager.self) private var appAccounts
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var routerPath: RouterPath
@Environment(Client.self) private var client
@Environment(CurrentAccount.self) private var currentAccount
@Environment(RouterPath.self) private var routerPath
@Environment(\.dismiss) private var dismiss
@StateObject private var viewModel: StatusEditorViewModel
@State private var viewModel: StatusEditorViewModel
@FocusState private var isSpoilerTextFocused: Bool
@State private var isDismissAlertPresented: Bool = false
@State private var isLanguageConfirmPresented = false
public init(mode: StatusEditorViewModel.Mode) {
_viewModel = StateObject(wrappedValue: .init(mode: mode))
_viewModel = .init(initialValue: .init(mode: mode))
}
public var body: some View {
@ -93,9 +93,9 @@ public struct StatusEditorView: View {
await viewModel.fetchCustomEmojis()
}
}
.onChange(of: currentAccount.account?.id, perform: { _ in
.onChange(of: currentAccount.account?.id) {
viewModel.currentAccount = currentAccount.account
})
}
.background(theme.primaryBackgroundColor)
.navigationTitle(viewModel.mode.title)
.navigationBarTitleDisplayMode(.inline)
@ -164,10 +164,10 @@ public struct StatusEditorView: View {
}
}
.interactiveDismissDisabled(viewModel.shouldDisplayDismissWarning)
.onChange(of: appAccounts.currentClient) { newClient in
.onChange(of: appAccounts.currentClient) { _, newValue in
if viewModel.mode.isInShareExtension {
currentAccount.setClient(client: newClient)
viewModel.client = newClient
currentAccount.setClient(client: newValue)
viewModel.client = newValue
}
}
}

View file

@ -8,7 +8,7 @@ import PhotosUI
import SwiftUI
@MainActor
public class StatusEditorViewModel: NSObject, ObservableObject {
@Observable public class StatusEditorViewModel: NSObject {
var mode: Mode
var client: Client?
@ -50,7 +50,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
return textView.markedTextRange
}
@Published var statusText = NSMutableAttributedString(string: "") {
var statusText = NSMutableAttributedString(string: "") {
didSet {
let range = selectedRange
processText()
@ -73,18 +73,18 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
private var itemsProvider: [NSItemProvider]?
@Published var backupStatusText: NSAttributedString?
var backupStatusText: NSAttributedString?
@Published var showPoll: Bool = false
@Published var pollVotingFrequency = PollVotingFrequency.oneVote
@Published var pollDuration = Duration.oneDay
@Published var pollOptions: [String] = ["", ""]
var showPoll: Bool = false
var pollVotingFrequency = PollVotingFrequency.oneVote
var pollDuration = Duration.oneDay
var pollOptions: [String] = ["", ""]
@Published var spoilerOn: Bool = false
@Published var spoilerText: String = ""
var spoilerOn: Bool = false
var spoilerText: String = ""
@Published var isPosting: Bool = false
@Published var selectedMedias: [PhotosPickerItem] = [] {
var isPosting: Bool = false
var selectedMedias: [PhotosPickerItem] = [] {
didSet {
if selectedMedias.count > 4 {
selectedMedias = selectedMedias.prefix(4).map { $0 }
@ -94,16 +94,16 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
}
}
@Published var isMediasLoading: Bool = false
var isMediasLoading: Bool = false
@Published var mediasImages: [StatusEditorMediaContainer] = []
@Published var replyToStatus: Status?
@Published var embeddedStatus: Status?
var mediasImages: [StatusEditorMediaContainer] = []
var replyToStatus: Status?
var embeddedStatus: Status?
@Published var customEmojis: [Emoji] = []
var customEmojis: [Emoji] = []
@Published var postingError: String?
@Published var showPostingErrorAlert: Bool = false
var postingError: String?
var showPostingErrorAlert: Bool = false
var canPost: Bool {
statusText.length > 0 || !mediasImages.isEmpty
@ -123,11 +123,11 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
return !modifiedStatusText.isEmpty && !mode.isInShareExtension
}
@Published var visibility: Models.Visibility = .pub
var visibility: Models.Visibility = .pub
@Published var mentionsSuggestions: [Account] = []
@Published var tagsSuggestions: [Tag] = []
@Published var selectedLanguage: String?
var mentionsSuggestions: [Account] = []
var tagsSuggestions: [Tag] = []
var selectedLanguage: String?
var hasExplicitlySelectedLanguage: Bool = false
private var currentSuggestionRange: NSRange?

View file

@ -23,10 +23,10 @@ public struct StatusEmbeddedView: View {
HStack {
VStack(alignment: .leading) {
makeAccountView(account: status.reblog?.account ?? status.account)
StatusRowView(viewModel: { .init(status: status,
client: client,
routerPath: routerPath,
showActions: false) })
StatusRowView(viewModel: .init(status: status,
client: client,
routerPath: routerPath,
showActions: false))
.accessibilityLabel(status.content.asRawText)
.environment(\.isCompact, true)
}

View file

@ -6,7 +6,7 @@ import SwiftUI
public struct StatusEditHistoryView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var theme: Theme
private let statusId: String

View file

@ -1,5 +1,6 @@
import Combine
import Models
import Observation
import SwiftUI
public enum StatusesState {
@ -13,7 +14,7 @@ public enum StatusesState {
}
@MainActor
public protocol StatusesFetcher: ObservableObject {
public protocol StatusesFetcher {
var statusesState: StatusesState { get }
func fetchNewestStatuses() async
func fetchNextPage() async

View file

@ -8,7 +8,7 @@ import SwiftUI
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
@EnvironmentObject private var theme: Theme
@ObservedObject private var fetcher: Fetcher
@State private var fetcher: Fetcher
// Whether this status is on a remote local timeline (many actions are unavailable if so)
private let isRemote: Bool
private let routerPath: RouterPath
@ -19,7 +19,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
routerPath: RouterPath,
isRemote: Bool = false)
{
self.fetcher = fetcher
_fetcher = .init(initialValue: fetcher)
self.isRemote = isRemote
self.client = client
self.routerPath = routerPath
@ -29,7 +29,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
switch fetcher.statusesState {
case .loading:
ForEach(Status.placeholders()) { status in
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.redacted(reason: .placeholder)
}
case .error:
@ -46,12 +46,10 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
case let .display(statuses, nextPageState):
ForEach(statuses, id: \.viewId) { status in
StatusRowView(viewModel: { StatusRowViewModel(status: status,
client: client,
routerPath: routerPath,
isRemote: isRemote)
})
StatusRowView(viewModel: StatusRowViewModel(status: status,
client: client,
routerPath: routerPath,
isRemote: isRemote))
.id(status.id)
.onAppear {
fetcher.statusDidAppear(status: status)

View file

@ -1,11 +1,12 @@
import AVKit
import DesignSystem
import Env
import Observation
import SwiftUI
@MainActor
class VideoPlayerViewModel: ObservableObject {
@Published var player: AVPlayer?
@Observable class VideoPlayerViewModel {
var player: AVPlayer?
private let url: URL
init(url: URL) {
@ -53,7 +54,7 @@ struct VideoPlayerView: View {
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var theme: Theme
@StateObject var viewModel: VideoPlayerViewModel
@State var viewModel: VideoPlayerViewModel
var body: some View {
ZStack {
@ -75,8 +76,8 @@ struct VideoPlayerView: View {
viewModel.pause()
}
.cornerRadius(4)
.onChange(of: scenePhase, perform: { scenePhase in
switch scenePhase {
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .background, .inactive:
viewModel.pause()
case .active:
@ -86,6 +87,6 @@ struct VideoPlayerView: View {
default:
break
}
})
}
}
}

View file

@ -6,15 +6,16 @@ import SwiftUI
public struct StatusPollView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var currentAccount: CurrentAccount
@StateObject private var viewModel: StatusPollViewModel
@Environment(Client.self) private var client
@Environment(CurrentInstance.self) private var currentInstance
@Environment(CurrentAccount.self) private var currentAccount
@State private var viewModel: StatusPollViewModel
private var status: AnyStatus
public init(poll: Poll, status: AnyStatus) {
_viewModel = StateObject(wrappedValue: .init(poll: poll))
_viewModel = .init(initialValue: .init(poll: poll))
self.status = status
}

View file

@ -1,15 +1,16 @@
import Combine
import Models
import Network
import Observation
import SwiftUI
@MainActor
public class StatusPollViewModel: ObservableObject {
@Observable public class StatusPollViewModel {
public var client: Client?
public var instance: Instance?
@Published var poll: Poll
@Published var votes: [Int] = []
var poll: Poll
var votes: [Int] = []
var showResults: Bool {
poll.ownVotes?.isEmpty == false || poll.expired

View file

@ -28,8 +28,8 @@ struct StatusActionButtonStyle: ButtonStyle {
SparklesView(counter: sparklesCounter, tint: tint, size: 5, velocity: 30)
}
}
.onChange(of: configuration.isPressed) { isPressed in
guard tintColor != nil, !isPressed, !isOn else { return }
.onChange(of: configuration.isPressed) { _, newValue in
guard tintColor != nil, !newValue, !isOn else { return }
withAnimation(.spring(response: 1, dampingFraction: 1)) {
sparklesCounter += 1
@ -88,8 +88,8 @@ struct StatusActionButtonStyle: ButtonStyle {
.onAppear {
cells = Self.generateCells()
}
.onChange(of: counter) { [counter] newCounter in
if floor(counter) != floor(newCounter) {
.onChange(of: counter) { oldValue, newValue in
if floor(oldValue) != floor(newValue) {
cells = Self.generateCells()
}
}

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