diff --git a/IceCubesApp/App/AppRouteur.swift b/IceCubesApp/App/AppRouteur.swift index 61812ed1..d827ca67 100644 --- a/IceCubesApp/App/AppRouteur.swift +++ b/IceCubesApp/App/AppRouteur.swift @@ -15,6 +15,8 @@ extension View { AccountDetailView(account: account) case let .statusDetail(id): StatusDetailView(statusId: id) + case let .hashTag(tag): + TimelineView(timeline: .hashtag(tag: tag)) } } } diff --git a/IceCubesApp/App/IceCubesApp.swift b/IceCubesApp/App/IceCubesApp.swift index d20f49d6..22ce718c 100644 --- a/IceCubesApp/App/IceCubesApp.swift +++ b/IceCubesApp/App/IceCubesApp.swift @@ -16,10 +16,12 @@ struct IceCubesApp: App { .tabItem { Label("Home", systemImage: "globe") } - NotificationsTab() - .tabItem { - Label("Notifications", systemImage: "bell") - } + if appAccountsManager.currentClient.isAuth { + NotificationsTab() + .tabItem { + Label("Notifications", systemImage: "bell") + } + } SettingsTabs() .tabItem { Label("Settings", systemImage: "gear") diff --git a/Packages/Network/Sources/Network/Client.swift b/Packages/Network/Sources/Network/Client.swift index 8f31caf3..3d99fb42 100644 --- a/Packages/Network/Sources/Network/Client.swift +++ b/Packages/Network/Sources/Network/Client.swift @@ -2,7 +2,13 @@ import Foundation import SwiftUI import Models -public class Client: ObservableObject { +public class Client: ObservableObject, Equatable { + public static func == (lhs: Client, rhs: Client) -> Bool { + lhs.isAuth == rhs.isAuth && + lhs.server == rhs.server && + lhs.oauthToken?.accessToken == rhs.oauthToken?.accessToken + } + public enum Version: String { case v1 } diff --git a/Packages/Network/Sources/Network/Endpoint/Timelines.swift b/Packages/Network/Sources/Network/Endpoint/Timelines.swift index e324b5f6..bd48c289 100644 --- a/Packages/Network/Sources/Network/Endpoint/Timelines.swift +++ b/Packages/Network/Sources/Network/Endpoint/Timelines.swift @@ -3,6 +3,7 @@ import Foundation public enum Timelines: Endpoint { case pub(sinceId: String?) case home(sinceId: String?) + case hashtag(tag: String, sinceId: String?) public func path() -> String { switch self { @@ -10,6 +11,8 @@ public enum Timelines: Endpoint { return "timelines/public" case .home: return "timelines/home" + case let .hashtag(tag, _): + return "timelines/tag/\(tag)" } } @@ -21,6 +24,9 @@ public enum Timelines: Endpoint { case .home(let sinceId): guard let sinceId else { return nil } return [.init(name: "max_id", value: sinceId)] + case let .hashtag(_, sinceId): + guard let sinceId else { return nil } + return [.init(name: "max_id", value: sinceId)] } } } diff --git a/Packages/Routeur/Sources/Routeur/Routeur.swift b/Packages/Routeur/Sources/Routeur/Routeur.swift index 876144b2..7773fd3a 100644 --- a/Packages/Routeur/Sources/Routeur/Routeur.swift +++ b/Packages/Routeur/Sources/Routeur/Routeur.swift @@ -6,6 +6,7 @@ public enum RouteurDestinations: Hashable { case accountDetail(id: String) case accountDetailWithAccount(account: Account) case statusDetail(id: String) + case hashTag(tag: String) } public enum SheetDestinations: Identifiable { @@ -30,7 +31,11 @@ public class RouterPath: ObservableObject { } public func handleStatus(status: AnyStatus, url: URL) -> OpenURLAction.Result { - if let mention = status.mentions.first(where: { $0.url == url }) { + if url.pathComponents.contains(where: { $0 == "tags" }), + let tag = url.pathComponents.last { + navigate(to: .hashTag(tag: tag)) + return .handled + } else if let mention = status.mentions.first(where: { $0.url == url }) { navigate(to: .accountDetail(id: mention.id)) return .handled } diff --git a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift new file mode 100644 index 00000000..0a24e313 --- /dev/null +++ b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift @@ -0,0 +1,36 @@ +import Foundation +import Models +import Network + +public enum TimelineFilter: Hashable, Equatable { + case pub, home + case hashtag(tag: String) + + public func hash(into hasher: inout Hasher) { + hasher.combine(title()) + } + + static func availableTimeline() -> [TimelineFilter] { + return [.pub, .home] + } + + func title() -> String { + switch self { + case .pub: + return "Public" + case .home: + return "Home" + case let .hashtag(tag): + return tag + } + } + + func endpoint(sinceId: String?) -> Timelines { + switch self { + case .pub: return .pub(sinceId: sinceId) + case .home: return .home(sinceId: sinceId) + case let .hashtag(tag): + return .hashtag(tag: tag, sinceId: sinceId) + } + } +} diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index 03ab4701..0f11ce18 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -8,9 +8,12 @@ import DesignSystem public struct TimelineView: View { @EnvironmentObject private var client: Client @StateObject private var viewModel = TimelineViewModel() - @State private var didAppear = false - public init() {} + private let filter: TimelineFilter? + + public init(timeline: TimelineFilter? = nil) { + self.filter = timeline + } public var body: some View { ScrollView { @@ -19,33 +22,38 @@ public struct TimelineView: View { } .padding(.top, DS.Constants.layoutPadding) } - .navigationTitle(viewModel.timeline.rawValue) + .navigationTitle(filter?.title() ?? viewModel.timeline.title()) .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - timelineFilterButton + if filter == nil { + ToolbarItem(placement: .navigationBarTrailing) { + timelineFilterButton + } } } - .task { + .onAppear { viewModel.client = client - if !didAppear { - await viewModel.fetchStatuses() - didAppear = true + if let filter { + viewModel.timeline = filter + } else { + viewModel.timeline = client.isAuth ? .home : .pub } } .refreshable { - await viewModel.fetchStatuses() + Task { + await viewModel.fetchStatuses() + } } } private var timelineFilterButton: some View { Menu { - ForEach(TimelineViewModel.TimelineFilter.allCases, id: \.self) { filter in + ForEach(TimelineFilter.availableTimeline(), id: \.self) { filter in Button { viewModel.timeline = filter } label: { - Text(filter.rawValue) + Text(filter.title()) } } } label: { diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index e4549d48..ef0606d6 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -5,30 +5,14 @@ import Status @MainActor class TimelineViewModel: ObservableObject, StatusesFetcher { - enum TimelineFilter: String, CaseIterable { - case pub = "Public" - case home = "Home" - - func endpoint(sinceId: String?) -> Timelines { - switch self { - case .pub: return .pub(sinceId: sinceId) - case .home: return .home(sinceId: sinceId) - } - } - } - - var client: Client? { - didSet { - timeline = client?.isAuth == true ? .home : .pub - } - } + var client: Client? private var statuses: [Status] = [] @Published var statusesState: StatusesState = .loading @Published var timeline: TimelineFilter = .pub { didSet { - if oldValue != timeline { + if oldValue != timeline || statuses.isEmpty { Task { await fetchStatuses() }