mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-26 00:50:38 +00:00
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:
parent
3853eff065
commit
4189a59cf6
116 changed files with 820 additions and 812 deletions
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -93,7 +93,7 @@ struct EditTagGroupView: View {
|
|||
.onSubmit {
|
||||
focusedField = Focus.new
|
||||
}
|
||||
.onChange(of: sfSymbolName) { _ in
|
||||
.onChange(of: sfSymbolName) {
|
||||
popupTagsPresented = true
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "Account",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>?
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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() {}
|
||||
|
||||
|
|
|
@ -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() {}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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] = []
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "AppAccount",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "Conversations",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "DesignSystem",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)?
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "Env",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -54,14 +54,14 @@ public extension EnvironmentValues {
|
|||
get { self[IsSupporter.self] }
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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() {}
|
||||
|
||||
|
|
|
@ -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() {}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "Explore",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "Lists",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "Models",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "Network",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "Notifications",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -7,7 +7,7 @@ let package = Package(
|
|||
name: "Status",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v17),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue