mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 08:20:59 +00:00
Search & Pin remote local timeline + general polish
This commit is contained in:
parent
27f0ee45b7
commit
f922ba344d
25 changed files with 493 additions and 158 deletions
|
@ -28,6 +28,8 @@
|
|||
9F7335EA2966B3F800AFF0BA /* Conversations in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7335E92966B3F800AFF0BA /* Conversations */; };
|
||||
9F7335ED2967463400AFF0BA /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7335EB2967461B00AFF0BA /* AVKit.framework */; };
|
||||
9F7335EF29674F7100AFF0BA /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7335EE29674F7100AFF0BA /* QuickLook.framework */; };
|
||||
9F7335F22967608F00AFF0BA /* AddRemoteTimelineVIew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7335F12967608F00AFF0BA /* AddRemoteTimelineVIew.swift */; };
|
||||
9F7335F72968274500AFF0BA /* AppAccountsSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7335F62968274500AFF0BA /* AppAccountsSelectorView.swift */; };
|
||||
9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAE4ACA293783B000772766 /* SettingsTab.swift */; };
|
||||
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAE4ACD29379A5A00772766 /* KeychainSwift */; };
|
||||
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAE4AD029379AD600772766 /* AppAccount.swift */; };
|
||||
|
@ -63,6 +65,8 @@
|
|||
9F7335E82966B3DC00AFF0BA /* Conversations */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Conversations; path = Packages/Conversations; sourceTree = "<group>"; };
|
||||
9F7335EB2967461B00AFF0BA /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.2.sdk/System/Library/Frameworks/AVKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
9F7335EE29674F7100AFF0BA /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.2.sdk/System/Library/Frameworks/QuickLook.framework; sourceTree = DEVELOPER_DIR; };
|
||||
9F7335F12967608F00AFF0BA /* AddRemoteTimelineVIew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRemoteTimelineVIew.swift; sourceTree = "<group>"; };
|
||||
9F7335F62968274500AFF0BA /* AppAccountsSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccountsSelectorView.swift; sourceTree = "<group>"; };
|
||||
9FAE4AC8293774FF00772766 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
9FAE4ACA293783B000772766 /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = "<group>"; };
|
||||
9FAE4AD029379AD600772766 /* AppAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccount.swift; sourceTree = "<group>"; };
|
||||
|
@ -118,11 +122,20 @@
|
|||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9F7335F02967607A00AFF0BA /* Timeline */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F398AB229360A4C00A889F2 /* TimelineTab.swift */,
|
||||
9F7335F12967608F00AFF0BA /* AddRemoteTimelineVIew.swift */,
|
||||
);
|
||||
path = Timeline;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9FAE4AC9293783A200772766 /* Tabs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F7335F02967607A00AFF0BA /* Timeline */,
|
||||
9FE151A4293C90EA00E9683D /* Settings */,
|
||||
9F398AB229360A4C00A889F2 /* TimelineTab.swift */,
|
||||
9F35DB4629506F6600B3281A /* NotificationTab.swift */,
|
||||
9F35DB4B2952005C00B3281A /* MessagesTab.swift */,
|
||||
9F55C68C2955968700F94077 /* ExploreTab.swift */,
|
||||
|
@ -138,6 +151,7 @@
|
|||
9FAE4AD22937A0C600772766 /* AppAccountsManager.swift */,
|
||||
9F2B92FE295EB87100DE16D0 /* AppAccountView.swift */,
|
||||
9F2B9300295EB8A100DE16D0 /* AppAccountViewModel.swift */,
|
||||
9F7335F62968274500AFF0BA /* AppAccountsSelectorView.swift */,
|
||||
);
|
||||
path = AppAccounts;
|
||||
sourceTree = "<group>";
|
||||
|
@ -301,6 +315,8 @@
|
|||
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
|
||||
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */,
|
||||
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */,
|
||||
9F7335F22967608F00AFF0BA /* AddRemoteTimelineVIew.swift in Sources */,
|
||||
9F7335F72968274500AFF0BA /* AppAccountsSelectorView.swift in Sources */,
|
||||
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
|
||||
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */,
|
||||
);
|
||||
|
|
64
IceCubesApp/App/AppAccounts/AppAccountsSelectorView.swift
Normal file
64
IceCubesApp/App/AppAccounts/AppAccountsSelectorView.swift
Normal file
|
@ -0,0 +1,64 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
import DesignSystem
|
||||
|
||||
struct AppAccountsSelectorView: View {
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||
|
||||
@ObservedObject var routeurPath: RouterPath
|
||||
|
||||
@State private var accountsViewModel: [AppAccountViewModel] = []
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if let account = currentAccount.account {
|
||||
routeurPath.navigate(to: .accountDetailWithAccount(account: account))
|
||||
}
|
||||
} label: {
|
||||
if let avatar = currentAccount.account?.avatar {
|
||||
AvatarView(url: avatar, size: .badge)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
refreshAccounts()
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(accountsViewModel, id: \.appAccount.id) { viewModel in
|
||||
Button {
|
||||
appAccounts.currentAccount = viewModel.appAccount
|
||||
} label: {
|
||||
HStack {
|
||||
if viewModel.account?.id == currentAccount.account?.id {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
Text("\(viewModel.account?.displayName ?? "")")
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
routeurPath.presentedSheet = .addAccount
|
||||
} label: {
|
||||
Label("Add Account", systemImage: "person.badge.plus")
|
||||
}
|
||||
}
|
||||
.onChange(of: currentAccount.account?.id) { _ in
|
||||
refreshAccounts()
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshAccounts() {
|
||||
if accountsViewModel.isEmpty || appAccounts.availableAccounts.count != accountsViewModel.count {
|
||||
accountsViewModel = []
|
||||
for account in appAccounts.availableAccounts {
|
||||
let viewModel: AppAccountViewModel = .init(appAccount: account)
|
||||
accountsViewModel.append(viewModel)
|
||||
Task {
|
||||
await viewModel.fetchAccount()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@ extension View {
|
|||
AccountDetailView(account: account)
|
||||
case let .statusDetail(id):
|
||||
StatusDetailView(statusId: id)
|
||||
case let .remoteStatusDetail(url):
|
||||
StatusDetailView(remoteStatusURL: url)
|
||||
case let .hashTag(tag, accountId):
|
||||
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)), scrollToTopSignal: .constant(0))
|
||||
case let .list(list):
|
||||
|
@ -49,6 +51,8 @@ extension View {
|
|||
ListEditView(list: list)
|
||||
case let .listAddAccount(account):
|
||||
ListAddAccountView(account: account)
|
||||
case .addAccount:
|
||||
AddAccountView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ struct IceCubesApp: App {
|
|||
@StateObject private var appAccountsManager = AppAccountsManager()
|
||||
@StateObject private var currentInstance = CurrentInstance()
|
||||
@StateObject private var currentAccount = CurrentAccount()
|
||||
@StateObject private var userPreferences = UserPreferences()
|
||||
@StateObject private var watcher = StreamWatcher()
|
||||
@StateObject private var quickLook = QuickLook()
|
||||
@StateObject private var theme = Theme()
|
||||
|
@ -39,6 +40,7 @@ struct IceCubesApp: App {
|
|||
.environmentObject(quickLook)
|
||||
.environmentObject(currentAccount)
|
||||
.environmentObject(currentInstance)
|
||||
.environmentObject(userPreferences)
|
||||
.environmentObject(theme)
|
||||
.environmentObject(watcher)
|
||||
.quickLookPreview($quickLook.url, in: quickLook.urls)
|
||||
|
|
|
@ -19,6 +19,9 @@ struct ExploreTab: View {
|
|||
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
|
||||
.toolbar {
|
||||
statusEditorToolbarItem(routeurPath: routeurPath, visibility: .pub)
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routeurPath: routeurPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
.environmentObject(routeurPath)
|
||||
|
|
|
@ -19,6 +19,11 @@ struct MessagesTab: View {
|
|||
ConversationsListView()
|
||||
.withAppRouteur()
|
||||
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routeurPath: routeurPath)
|
||||
}
|
||||
}
|
||||
.id(currentAccount.account?.id)
|
||||
}
|
||||
.environmentObject(routeurPath)
|
||||
|
|
|
@ -18,6 +18,9 @@ struct NotificationsTab: View {
|
|||
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
|
||||
.toolbar {
|
||||
statusEditorToolbarItem(routeurPath: routeurPath, visibility: .pub)
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routeurPath: routeurPath)
|
||||
}
|
||||
}
|
||||
.id(currentAccount.account?.id)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ struct AddAccountView: View {
|
|||
.keyboardType(.URL)
|
||||
.textContentType(.URL)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused($isInstanceURLFieldFocused)
|
||||
if let instanceFetchError {
|
||||
Text(instanceFetchError)
|
||||
|
|
102
IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineVIew.swift
Normal file
102
IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineVIew.swift
Normal file
|
@ -0,0 +1,102 @@
|
|||
import SwiftUI
|
||||
import Network
|
||||
import Models
|
||||
import Env
|
||||
import DesignSystem
|
||||
import NukeUI
|
||||
import Shimmer
|
||||
|
||||
struct AddRemoteTimelineVIew: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@State private var instanceName: String = ""
|
||||
@State private var instance: Instance?
|
||||
@State private var instances: [InstanceSocial] = []
|
||||
|
||||
@FocusState private var isInstanceURLFieldFocused: Bool
|
||||
|
||||
@Binding var addedInstance: String
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
TextField("Instance URL", text: $instanceName)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.keyboardType(.URL)
|
||||
.textContentType(.URL)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused($isInstanceURLFieldFocused)
|
||||
if let instance {
|
||||
Label("\(instance.title) is a valid instance", systemImage: "checkmark.seal.fill")
|
||||
.foregroundColor(.green)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
Button {
|
||||
guard instance != nil else { return }
|
||||
addedInstance = instanceName
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Add")
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
instancesListView
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle("Add remote local timeline")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel", action: { dismiss() })
|
||||
}
|
||||
}
|
||||
.onChange(of: instanceName, perform: { newValue in
|
||||
Task {
|
||||
let client = Client(server: newValue)
|
||||
instance = try? await client.get(endpoint: Instances.instance)
|
||||
}
|
||||
})
|
||||
.onAppear {
|
||||
isInstanceURLFieldFocused = true
|
||||
let client = InstanceSocialClient()
|
||||
Task {
|
||||
self.instances = await client.fetchInstances()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var instancesListView: some View {
|
||||
Section("Suggestions") {
|
||||
if instances.isEmpty {
|
||||
ProgressView()
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
} else {
|
||||
ForEach(instanceName.isEmpty ? instances : instances.filter{ $0.name.contains(instanceName.lowercased()) }) { instance in
|
||||
Button {
|
||||
self.instanceName = instance.name
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(instance.name)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Text(instance.info?.shortDescription ?? "")
|
||||
.font(.body)
|
||||
.foregroundColor(.gray)
|
||||
Text("\(instance.users) users ⸱ \(instance.statuses) posts")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,17 +7,19 @@ import DesignSystem
|
|||
import Models
|
||||
|
||||
struct TimelineTab: View {
|
||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
@EnvironmentObject private var client: Client
|
||||
@StateObject private var routeurPath = RouterPath()
|
||||
@Binding var popToRootTab: Tab
|
||||
|
||||
@State private var didAppear: Bool = false
|
||||
@State private var timeline: TimelineFilter = .home
|
||||
@State private var scrollToTopSignal: Int = 0
|
||||
@State private var isAddAccountSheetDisplayed = false
|
||||
@State private var accountsViewModel: [AppAccountViewModel] = []
|
||||
|
||||
@State private var newlyAddedLocalTimeline: String = ""
|
||||
@State private var isAddRemoteLocalTimelinePresented: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $routeurPath.path) {
|
||||
|
@ -25,33 +27,29 @@ struct TimelineTab: View {
|
|||
.withAppRouteur()
|
||||
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
|
||||
.toolbar {
|
||||
ToolbarTitleMenu {
|
||||
timelineFilterButton
|
||||
}
|
||||
if client.isAuth {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
accountButton
|
||||
}
|
||||
statusEditorToolbarItem(routeurPath: routeurPath, visibility: .pub)
|
||||
} else {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
addAccountButton
|
||||
}
|
||||
}
|
||||
toolbarView
|
||||
}
|
||||
.id(currentAccount.account?.id)
|
||||
}
|
||||
.sheet(isPresented: $isAddAccountSheetDisplayed) {
|
||||
AddAccountView()
|
||||
.sheet(isPresented: $isAddRemoteLocalTimelinePresented) {
|
||||
AddRemoteTimelineVIew(addedInstance: $newlyAddedLocalTimeline)
|
||||
}
|
||||
.onAppear {
|
||||
routeurPath.client = client
|
||||
timeline = client.isAuth ? .home : .pub
|
||||
if !didAppear {
|
||||
didAppear = true
|
||||
timeline = client.isAuth ? .home : .federated
|
||||
}
|
||||
Task {
|
||||
await currentAccount.fetchLists()
|
||||
}
|
||||
}
|
||||
.environmentObject(routeurPath)
|
||||
.onChange(of: client.isAuth, perform: { isAuth in
|
||||
timeline = isAuth ? .home : .federated
|
||||
})
|
||||
.onChange(of: currentAccount.account?.id, perform: { _ in
|
||||
timeline = client.isAuth ? .home : .federated
|
||||
})
|
||||
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
||||
if popToRootTab == .timeline {
|
||||
if routeurPath.path.isEmpty {
|
||||
|
@ -64,6 +62,14 @@ struct TimelineTab: View {
|
|||
.onChange(of: currentAccount.account?.id) { _ in
|
||||
routeurPath.path = []
|
||||
}
|
||||
.onChange(of: isAddRemoteLocalTimelinePresented) { isPresented in
|
||||
if !isPresented && !newlyAddedLocalTimeline.isEmpty {
|
||||
preferences.remoteLocalTimelines.append(newlyAddedLocalTimeline)
|
||||
timeline = .remoteLocal(server: newlyAddedLocalTimeline)
|
||||
newlyAddedLocalTimeline = ""
|
||||
}
|
||||
}
|
||||
.environmentObject(routeurPath)
|
||||
}
|
||||
|
||||
|
||||
|
@ -99,58 +105,72 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var accountButton: some View {
|
||||
|
||||
if !preferences.remoteLocalTimelines.isEmpty {
|
||||
Menu("Local Timelines") {
|
||||
ForEach(preferences.remoteLocalTimelines, id: \.self) { server in
|
||||
Button {
|
||||
timeline = .remoteLocal(server: server)
|
||||
} label: {
|
||||
Label(server, systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
if let account = currentAccount.account {
|
||||
routeurPath.navigate(to: .accountDetailWithAccount(account: account))
|
||||
}
|
||||
isAddRemoteLocalTimelinePresented = true
|
||||
} label: {
|
||||
if let avatar = currentAccount.account?.avatar {
|
||||
AvatarView(url: avatar, size: .badge)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if accountsViewModel.isEmpty || appAccounts.availableAccounts.count != accountsViewModel.count {
|
||||
accountsViewModel = []
|
||||
for account in appAccounts.availableAccounts {
|
||||
let viewModel: AppAccountViewModel = .init(appAccount: account)
|
||||
accountsViewModel.append(viewModel)
|
||||
Task {
|
||||
await viewModel.fetchAccount()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(accountsViewModel, id: \.appAccount.id) { viewModel in
|
||||
Button {
|
||||
appAccounts.currentAccount = viewModel.appAccount
|
||||
timeline = .home
|
||||
} label: {
|
||||
HStack {
|
||||
if viewModel.account?.id == currentAccount.account?.id {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
Text("\(viewModel.account?.displayName ?? "")")
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
isAddAccountSheetDisplayed = true
|
||||
} label: {
|
||||
Label("Add Account", systemImage: "person.badge.plus")
|
||||
}
|
||||
Label("Add a local timeline", systemImage: "badge.plus.radiowaves.right")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private var addAccountButton: some View {
|
||||
Button {
|
||||
isAddAccountSheetDisplayed = true
|
||||
routeurPath.presentedSheet = .addAccount
|
||||
} label: {
|
||||
Image(systemName: "person.badge.plus")
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbarView: some ToolbarContent {
|
||||
ToolbarTitleMenu {
|
||||
timelineFilterButton
|
||||
}
|
||||
if client.isAuth {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routeurPath: routeurPath)
|
||||
}
|
||||
statusEditorToolbarItem(routeurPath: routeurPath, visibility: .pub)
|
||||
} else {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
addAccountButton
|
||||
}
|
||||
}
|
||||
switch timeline {
|
||||
case let .list(list):
|
||||
ToolbarItem {
|
||||
Button {
|
||||
routeurPath.presentedSheet = .listEdit(list: list)
|
||||
} label: {
|
||||
Image(systemName: "list.bullet")
|
||||
}
|
||||
}
|
||||
case let .remoteLocal(server):
|
||||
ToolbarItem {
|
||||
Button {
|
||||
preferences.remoteLocalTimelines.removeAll(where: { $0 == server })
|
||||
timeline = client.isAuth ? .home : .federated
|
||||
} label: {
|
||||
Image(systemName: "pin.slash")
|
||||
}
|
||||
}
|
||||
default:
|
||||
ToolbarItem {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,20 +32,26 @@ struct AccountDetailHeaderView: View {
|
|||
private var headerImageView: some View {
|
||||
GeometryReader { proxy in
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
LazyImage(url: account.header) { state in
|
||||
if let image = state.image {
|
||||
image
|
||||
.resizingMode(.aspectFill)
|
||||
} else if state.isLoading {
|
||||
Color.gray
|
||||
.frame(height: bannerHeight)
|
||||
.shimmering()
|
||||
} else {
|
||||
Color.gray
|
||||
.frame(height: bannerHeight)
|
||||
if reasons.contains(.placeholder) {
|
||||
Rectangle()
|
||||
.foregroundColor(.gray)
|
||||
.frame(height: bannerHeight)
|
||||
} else {
|
||||
LazyImage(url: account.header) { state in
|
||||
if let image = state.image {
|
||||
image
|
||||
.resizingMode(.aspectFill)
|
||||
} else if state.isLoading {
|
||||
Color.gray
|
||||
.frame(height: bannerHeight)
|
||||
.shimmering()
|
||||
} else {
|
||||
Color.gray
|
||||
.frame(height: bannerHeight)
|
||||
}
|
||||
}
|
||||
.frame(height: bannerHeight)
|
||||
}
|
||||
.frame(height: bannerHeight)
|
||||
|
||||
if relationship?.followedBy == true {
|
||||
Text("Follows You")
|
||||
|
|
|
@ -112,6 +112,7 @@ public struct AccountDetailView: View {
|
|||
scrollViewProxy: proxy,
|
||||
scrollOffset: $scrollOffset)
|
||||
.redacted(reason: .placeholder)
|
||||
.shimmering()
|
||||
case let .data(account):
|
||||
AccountDetailHeaderView(isCurrentUser: isCurrentUser,
|
||||
account: account,
|
||||
|
|
|
@ -3,6 +3,7 @@ import Shimmer
|
|||
import NukeUI
|
||||
|
||||
public struct AvatarView: View {
|
||||
@Environment(\.redactionReasons) private var reasons
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
public enum Size {
|
||||
|
@ -33,7 +34,6 @@ public struct AvatarView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@Environment(\.redactionReasons) private var reasons
|
||||
public let url: URL
|
||||
public let size: Size
|
||||
|
||||
|
@ -47,7 +47,7 @@ public struct AvatarView: View {
|
|||
if reasons == .placeholder {
|
||||
RoundedRectangle(cornerRadius: size.cornerRadius)
|
||||
.fill(.gray)
|
||||
.frame(maxWidth: size.size.width, maxHeight: size.size.height)
|
||||
.frame(width: size.size.width, height: size.size.height)
|
||||
} else {
|
||||
LazyImage(url: url) { state in
|
||||
if let image = state.image {
|
||||
|
|
21
Packages/Env/Sources/Env/Ext/AppStorage.swift
Normal file
21
Packages/Env/Sources/Env/Ext/AppStorage.swift
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Foundation
|
||||
|
||||
extension Array: RawRepresentable where Element: Codable {
|
||||
public init?(rawValue: String) {
|
||||
guard let data = rawValue.data(using: .utf8),
|
||||
let result = try? JSONDecoder().decode([Element].self, from: data)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
self = result
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
guard let data = try? JSONEncoder().encode(self),
|
||||
let result = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
return "[]"
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ public enum RouteurDestinations: Hashable {
|
|||
case accountDetail(id: String)
|
||||
case accountDetailWithAccount(account: Account)
|
||||
case statusDetail(id: String)
|
||||
case remoteStatusDetail(url: URL)
|
||||
case hashTag(tag: String, account: String?)
|
||||
case list(list: Models.List)
|
||||
case followers(id: String)
|
||||
|
@ -23,6 +24,7 @@ public enum SheetDestinations: Identifiable {
|
|||
case mentionStatusEditor(account: Account, visibility: Models.Visibility)
|
||||
case listEdit(list: Models.List)
|
||||
case listAddAccount(account: Account)
|
||||
case addAccount
|
||||
|
||||
public var id: String {
|
||||
switch self {
|
||||
|
@ -32,6 +34,8 @@ public enum SheetDestinations: Identifiable {
|
|||
return "listEdit"
|
||||
case .listAddAccount:
|
||||
return "listAddAccount"
|
||||
case .addAccount:
|
||||
return "addAccount"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,9 +66,7 @@ public class RouterPath: ObservableObject {
|
|||
if url.absoluteString.contains(client.server) {
|
||||
navigate(to: .statusDetail(id: String(id)))
|
||||
} else {
|
||||
Task {
|
||||
await navigateToStatusFrom(url: url)
|
||||
}
|
||||
navigate(to: .remoteStatusDetail(url: url))
|
||||
}
|
||||
return .handled
|
||||
}
|
||||
|
@ -85,23 +87,7 @@ public class RouterPath: ObservableObject {
|
|||
}
|
||||
return .systemAction
|
||||
}
|
||||
|
||||
public func navigateToStatusFrom(url: URL) async {
|
||||
guard let client else { return }
|
||||
Task {
|
||||
let results: SearchResults? = try? await client.get(endpoint: Search.search(query: url.absoluteString,
|
||||
type: "statuses",
|
||||
offset: nil,
|
||||
following: nil),
|
||||
forceVersion: .v2)
|
||||
if let status = results?.statuses.first {
|
||||
navigate(to: .statusDetail(id: status.id))
|
||||
} else {
|
||||
await UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func navigateToAccountFrom(acct: String, url: URL) async {
|
||||
guard let client else { return }
|
||||
Task {
|
||||
|
@ -117,4 +103,20 @@ public class RouterPath: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func navigateToAccountFrom(url: URL) async {
|
||||
guard let client else { return }
|
||||
Task {
|
||||
let results: SearchResults? = try? await client.get(endpoint: Search.search(query: url.absoluteString,
|
||||
type: "accounts",
|
||||
offset: nil,
|
||||
following: nil),
|
||||
forceVersion: .v2)
|
||||
if let account = results?.accounts.first {
|
||||
navigate(to: .accountDetailWithAccount(account: account))
|
||||
} else {
|
||||
await UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
8
Packages/Env/Sources/Env/UserPreferences.swift
Normal file
8
Packages/Env/Sources/Env/UserPreferences.swift
Normal file
|
@ -0,0 +1,8 @@
|
|||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
public class UserPreferences: ObservableObject {
|
||||
@AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = []
|
||||
|
||||
public init() { }
|
||||
}
|
|
@ -18,6 +18,10 @@ public struct StatusDetailView: View {
|
|||
_viewModel = StateObject(wrappedValue: .init(statusId: statusId))
|
||||
}
|
||||
|
||||
public init(remoteStatusURL: URL) {
|
||||
_viewModel = StateObject(wrappedValue: .init(remoteStatusURL: remoteStatusURL))
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
|
@ -70,7 +74,10 @@ public struct StatusDetailView: View {
|
|||
guard !isLoaded else { return }
|
||||
isLoaded = true
|
||||
viewModel.client = client
|
||||
await viewModel.fetchStatusDetail()
|
||||
let result = await viewModel.fetch()
|
||||
if !result {
|
||||
_ = routeurPath.path.popLast()
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
proxy.scrollTo(viewModel.statusId, anchor: .center)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ import Network
|
|||
|
||||
@MainActor
|
||||
class StatusDetailViewModel: ObservableObject {
|
||||
public let statusId: String
|
||||
public var statusId: String?
|
||||
public var remoteStatusURL: URL?
|
||||
|
||||
var client: Client?
|
||||
|
||||
|
@ -19,10 +20,44 @@ class StatusDetailViewModel: ObservableObject {
|
|||
init(statusId: String) {
|
||||
state = .loading
|
||||
self.statusId = statusId
|
||||
self.remoteStatusURL = nil
|
||||
}
|
||||
|
||||
func fetchStatusDetail() async {
|
||||
guard let client else { return }
|
||||
init(remoteStatusURL: URL) {
|
||||
state = .loading
|
||||
self.remoteStatusURL = remoteStatusURL
|
||||
self.statusId = nil
|
||||
}
|
||||
|
||||
func fetch() async -> Bool {
|
||||
if statusId != nil {
|
||||
await fetchStatusDetail()
|
||||
return true
|
||||
} else if remoteStatusURL != nil {
|
||||
return await fetchRemoteStatus()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func fetchRemoteStatus() async -> Bool {
|
||||
guard let client, let remoteStatusURL else { return false }
|
||||
let results: SearchResults? = try? await client.get(endpoint: Search.search(query: remoteStatusURL.absoluteString,
|
||||
type: "statuses",
|
||||
offset: nil,
|
||||
following: nil),
|
||||
forceVersion: .v2)
|
||||
if let statusId = results?.statuses.first?.id {
|
||||
self.statusId = statusId
|
||||
await fetchStatusDetail()
|
||||
return true
|
||||
} else {
|
||||
await UIApplication.shared.open(remoteStatusURL)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchStatusDetail() async {
|
||||
guard let client, let statusId else { return }
|
||||
do {
|
||||
let status: Status = try await client.get(endpoint: Statuses.status(id: statusId))
|
||||
let context: StatusContext = try await client.get(endpoint: Statuses.context(id: statusId))
|
||||
|
|
|
@ -5,9 +5,11 @@ import DesignSystem
|
|||
|
||||
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||
@ObservedObject private var fetcher: Fetcher
|
||||
private let isRemote: Bool
|
||||
|
||||
public init(fetcher: Fetcher) {
|
||||
public init(fetcher: Fetcher, isRemote: Bool = false) {
|
||||
self.fetcher = fetcher
|
||||
self.isRemote = isRemote
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
|
@ -26,7 +28,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
|||
Text(error.localizedDescription)
|
||||
case let .display(statuses, nextPageState):
|
||||
ForEach(statuses, id: \.viewId) { status in
|
||||
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
||||
StatusRowView(viewModel: .init(status: status, isCompact: false, isRemote: isRemote))
|
||||
.id(status.id)
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
Divider()
|
||||
|
|
|
@ -8,26 +8,29 @@ struct StatusRowContextMenu: View {
|
|||
@ObservedObject var viewModel: StatusRowViewModel
|
||||
|
||||
var body: some View {
|
||||
Button { Task {
|
||||
if viewModel.isFavourited {
|
||||
await viewModel.unFavourite()
|
||||
} else {
|
||||
await viewModel.favourite()
|
||||
if !viewModel.isRemote {
|
||||
Button { Task {
|
||||
if viewModel.isFavourited {
|
||||
await viewModel.unFavourite()
|
||||
} else {
|
||||
await viewModel.favourite()
|
||||
}
|
||||
} } label: {
|
||||
Label(viewModel.isFavourited ? "Unfavorite" : "Favorite", systemImage: "star")
|
||||
}
|
||||
} } label: {
|
||||
Label(viewModel.isFavourited ? "Unfavorite" : "Favorite", systemImage: "star")
|
||||
}
|
||||
Button { Task {
|
||||
if viewModel.isReblogged {
|
||||
await viewModel.unReblog()
|
||||
} else {
|
||||
await viewModel.reblog()
|
||||
Button { Task {
|
||||
if viewModel.isReblogged {
|
||||
await viewModel.unReblog()
|
||||
} else {
|
||||
await viewModel.reblog()
|
||||
}
|
||||
} } label: {
|
||||
Label(viewModel.isReblogged ? "Unboost" : "Boost", systemImage: "arrow.left.arrow.right.circle")
|
||||
}
|
||||
} } label: {
|
||||
Label(viewModel.isReblogged ? "Unboost" : "Boost", systemImage: "arrow.left.arrow.right.circle")
|
||||
|
||||
}
|
||||
|
||||
if viewModel.status.visibility == .pub {
|
||||
if viewModel.status.visibility == .pub, !viewModel.isRemote {
|
||||
Button {
|
||||
routeurPath.presentedSheet = .quoteStatusEditor(status: viewModel.status)
|
||||
} label: {
|
||||
|
@ -63,7 +66,7 @@ struct StatusRowContextMenu: View {
|
|||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if !viewModel.isRemote {
|
||||
Section(viewModel.status.account.acct) {
|
||||
Button {
|
||||
routeurPath.presentedSheet = .mentionStatusEditor(account: viewModel.status.account, visibility: .pub)
|
||||
|
|
|
@ -42,13 +42,13 @@ public struct StatusRowView: View {
|
|||
replyView
|
||||
}
|
||||
statusView
|
||||
if !viewModel.isCompact && viewModel.showActions {
|
||||
if !viewModel.isCompact && !viewModel.isRemote {
|
||||
StatusActionsView(viewModel: viewModel)
|
||||
.padding(.vertical, 8)
|
||||
.tint(viewModel.isFocused ? theme.tintColor : .gray)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
routeurPath.navigate(to: .statusDetail(id: viewModel.status.reblog?.id ?? viewModel.status.id))
|
||||
viewModel.navigateToDetail(routeurPath: routeurPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +93,13 @@ public struct StatusRowView: View {
|
|||
.foregroundColor(.gray)
|
||||
.fontWeight(.semibold)
|
||||
.onTapGesture {
|
||||
routeurPath.navigate(to: .accountDetailWithAccount(account: viewModel.status.account))
|
||||
if viewModel.isRemote, let url = viewModel.status.account.url {
|
||||
Task {
|
||||
await routeurPath.navigateToAccountFrom(url: url)
|
||||
}
|
||||
} else {
|
||||
routeurPath.navigate(to: .accountDetailWithAccount(account: viewModel.status.account))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +117,13 @@ public struct StatusRowView: View {
|
|||
.foregroundColor(.gray)
|
||||
.fontWeight(.semibold)
|
||||
.onTapGesture {
|
||||
routeurPath.navigate(to: .accountDetail(id: mention.id))
|
||||
if viewModel.isRemote {
|
||||
Task {
|
||||
await routeurPath.navigateToAccountFrom(url: mention.url)
|
||||
}
|
||||
} else {
|
||||
routeurPath.navigate(to: .accountDetail(id: mention.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +134,13 @@ public struct StatusRowView: View {
|
|||
if !viewModel.isCompact {
|
||||
HStack(alignment: .top) {
|
||||
Button {
|
||||
routeurPath.navigate(to: .accountDetailWithAccount(account: status.account))
|
||||
if viewModel.isRemote, let url = status.account.url {
|
||||
Task {
|
||||
await routeurPath.navigateToAccountFrom(url: url)
|
||||
}
|
||||
} else {
|
||||
routeurPath.navigate(to: .accountDetailWithAccount(account: status.account))
|
||||
}
|
||||
} label: {
|
||||
accountView(status: status)
|
||||
}.buttonStyle(.plain)
|
||||
|
@ -180,7 +198,7 @@ public struct StatusRowView: View {
|
|||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
routeurPath.navigate(to: .statusDetail(id: viewModel.status.reblog?.id ?? viewModel.status.id))
|
||||
viewModel.navigateToDetail(routeurPath: routeurPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import Network
|
||||
import Env
|
||||
|
||||
@MainActor
|
||||
public class StatusRowViewModel: ObservableObject {
|
||||
let status: Status
|
||||
let isCompact: Bool
|
||||
let isFocused: Bool
|
||||
let showActions: Bool
|
||||
let isRemote: Bool
|
||||
|
||||
@Published var favouritesCount: Int
|
||||
@Published var isFavourited: Bool
|
||||
|
@ -29,11 +30,11 @@ public class StatusRowViewModel: ObservableObject {
|
|||
public init(status: Status,
|
||||
isCompact: Bool = false,
|
||||
isFocused: Bool = false,
|
||||
showActions: Bool = true) {
|
||||
isRemote: Bool = false) {
|
||||
self.status = status
|
||||
self.isCompact = isCompact
|
||||
self.isFocused = isFocused
|
||||
self.showActions = showActions
|
||||
self.isRemote = isRemote
|
||||
if let reblog = status.reblog {
|
||||
self.isFavourited = reblog.favourited == true
|
||||
self.isReblogged = reblog.reblogged == true
|
||||
|
@ -51,6 +52,14 @@ public class StatusRowViewModel: ObservableObject {
|
|||
self.isFiltered = filter != nil
|
||||
}
|
||||
|
||||
func navigateToDetail(routeurPath: RouterPath) {
|
||||
if isRemote, let url = status.reblog?.url ?? status.url {
|
||||
routeurPath.navigate(to: .remoteStatusDetail(url: url))
|
||||
} else {
|
||||
routeurPath.navigate(to: .statusDetail(id: status.reblog?.id ?? status.id))
|
||||
}
|
||||
}
|
||||
|
||||
func loadEmbededStatus() async {
|
||||
guard let client,
|
||||
let urls = status.content.findStatusesURLs(),
|
||||
|
|
|
@ -3,9 +3,10 @@ import Models
|
|||
import Network
|
||||
|
||||
public enum TimelineFilter: Hashable, Equatable {
|
||||
case pub, local, home, trending
|
||||
case federated, local, home, trending
|
||||
case hashtag(tag: String, accountId: String?)
|
||||
case list(list: List)
|
||||
case remoteLocal(server: String)
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(title())
|
||||
|
@ -13,14 +14,14 @@ public enum TimelineFilter: Hashable, Equatable {
|
|||
|
||||
public static func availableTimeline(client: Client) -> [TimelineFilter] {
|
||||
if !client.isAuth {
|
||||
return [.pub, .local, .trending]
|
||||
return [.federated, .local, .trending]
|
||||
}
|
||||
return [.pub, .local, .trending, .home]
|
||||
return [.federated, .local, .trending, .home]
|
||||
}
|
||||
|
||||
public func title() -> String {
|
||||
switch self {
|
||||
case .pub:
|
||||
case .federated:
|
||||
return "Federated"
|
||||
case .local:
|
||||
return "Local"
|
||||
|
@ -32,12 +33,14 @@ public enum TimelineFilter: Hashable, Equatable {
|
|||
return "#\(tag)"
|
||||
case let .list(list):
|
||||
return list.title
|
||||
case let .remoteLocal(server):
|
||||
return server
|
||||
}
|
||||
}
|
||||
|
||||
public func iconName() -> String? {
|
||||
switch self {
|
||||
case .pub:
|
||||
case .federated:
|
||||
return "globe.americas"
|
||||
case .local:
|
||||
return "person.3"
|
||||
|
@ -47,6 +50,8 @@ public enum TimelineFilter: Hashable, Equatable {
|
|||
return "house"
|
||||
case .list(_):
|
||||
return "list.bullet"
|
||||
case .remoteLocal:
|
||||
return "dot.radiowaves.right"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
@ -54,8 +59,9 @@ public enum TimelineFilter: Hashable, Equatable {
|
|||
|
||||
public func endpoint(sinceId: String?, maxId: String?, minId: String?, offset: Int?) -> Endpoint {
|
||||
switch self {
|
||||
case .pub: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false)
|
||||
case .federated: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false)
|
||||
case .local: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true)
|
||||
case .remoteLocal: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true)
|
||||
case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId)
|
||||
case .trending: return Trends.statuses(offset: offset)
|
||||
case let .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId)
|
||||
|
|
|
@ -41,7 +41,12 @@ public struct TimelineView: View {
|
|||
LazyVStack {
|
||||
tagHeaderView
|
||||
.padding(.bottom, 16)
|
||||
StatusesListView(fetcher: viewModel)
|
||||
switch viewModel.timeline {
|
||||
case .remoteLocal:
|
||||
StatusesListView(fetcher: viewModel, isRemote: true)
|
||||
default:
|
||||
StatusesListView(fetcher: viewModel)
|
||||
}
|
||||
}
|
||||
.padding(.top, .layoutPadding)
|
||||
}
|
||||
|
@ -55,33 +60,19 @@ public struct TimelineView: View {
|
|||
}
|
||||
}
|
||||
.navigationTitle(timeline.title())
|
||||
.toolbar{
|
||||
switch timeline {
|
||||
case let .list(list):
|
||||
ToolbarItem {
|
||||
Button {
|
||||
routerPath.presentedSheet = .listEdit(list: list)
|
||||
} label: {
|
||||
Image(systemName: "pencil")
|
||||
}
|
||||
}
|
||||
default:
|
||||
ToolbarItem {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear {
|
||||
viewModel.client = client
|
||||
viewModel.timeline = timeline
|
||||
if viewModel.client == nil {
|
||||
viewModel.client = client
|
||||
viewModel.timeline = timeline
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
feedbackGenerator.impactOccurred(intensity: 0.3)
|
||||
await viewModel.fetchStatuses(userIntent: true)
|
||||
feedbackGenerator.impactOccurred(intensity: 0.7)
|
||||
}
|
||||
.onChange(of: watcher.latestEvent?.id) { id in
|
||||
.onChange(of: watcher.latestEvent?.id) { _ in
|
||||
if let latestEvent = watcher.latestEvent {
|
||||
viewModel.handleEvent(event: latestEvent, currentAccount: account)
|
||||
}
|
||||
|
@ -92,7 +83,13 @@ public struct TimelineView: View {
|
|||
}
|
||||
})
|
||||
.onChange(of: timeline) { newTimeline in
|
||||
viewModel.timeline = timeline
|
||||
switch newTimeline {
|
||||
case let .remoteLocal(server):
|
||||
viewModel.client = Client(server: server)
|
||||
default:
|
||||
viewModel.client = client
|
||||
}
|
||||
viewModel.timeline = newTimeline
|
||||
}
|
||||
.onChange(of: scenePhase, perform: { scenePhase in
|
||||
switch scenePhase {
|
||||
|
|
|
@ -18,7 +18,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
|||
private var statuses: [Status] = []
|
||||
|
||||
@Published var statusesState: StatusesState = .loading
|
||||
@Published var timeline: TimelineFilter = .pub {
|
||||
@Published var timeline: TimelineFilter = .federated {
|
||||
didSet {
|
||||
Task {
|
||||
if oldValue != timeline {
|
||||
|
|
Loading…
Reference in a new issue