IceCubesApp/Packages/Env/Sources/Env/Router.swift
Thomas Ricouard 1f858414d8 format .
2024-02-14 12:48:14 +01:00

201 lines
6.8 KiB
Swift

import Combine
import Foundation
import Models
import Network
import Observation
import SwiftUI
public enum RouterDestination: Hashable {
case accountDetail(id: String)
case accountDetailWithAccount(account: Account)
case accountSettingsWithAccount(account: Account, appAccount: AppAccount)
case statusDetail(id: String)
case statusDetailWithStatus(status: Status)
case remoteStatusDetail(url: URL)
case conversationDetail(conversation: Conversation)
case hashTag(tag: String, account: String?)
case list(list: Models.List)
case followers(id: String)
case following(id: String)
case favoritedBy(id: String)
case rebloggedBy(id: String)
case accountsList(accounts: [Account])
case trendingTimeline
case trendingLinks(cards: [Card])
case tagsList(tags: [Tag])
}
public enum WindowDestinationEditor: Hashable, Codable {
case newStatusEditor(visibility: Models.Visibility)
case editStatusEditor(status: Status)
case replyToStatusEditor(status: Status)
case quoteStatusEditor(status: Status)
case mentionStatusEditor(account: Account, visibility: Models.Visibility)
case quoteLinkStatusEditor(link: URL)
}
public enum WindowDestinationMedia: Hashable, Codable {
case mediaViewer(attachments: [MediaAttachment], selectedAttachment: MediaAttachment)
}
public enum SheetDestination: Identifiable {
case newStatusEditor(visibility: Models.Visibility)
case editStatusEditor(status: Status)
case replyToStatusEditor(status: Status)
case quoteStatusEditor(status: Status)
case quoteLinkStatusEditor(link: URL)
case mentionStatusEditor(account: Account, visibility: Models.Visibility)
case listCreate
case listEdit(list: Models.List)
case listAddAccount(account: Account)
case addAccount
case addRemoteLocalTimeline
case addTagGroup
case statusEditHistory(status: String)
case settings
case about
case support
case accountPushNotficationsSettings
case report(status: Status)
case shareImage(image: UIImage, status: Status)
case editTagGroup(tagGroup: TagGroup, onSaved: ((TagGroup) -> Void)?)
case timelineContentFilter
public var id: String {
switch self {
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
.mentionStatusEditor, .quoteLinkStatusEditor:
"statusEditor"
case .listCreate:
"listCreate"
case .listEdit:
"listEdit"
case .listAddAccount:
"listAddAccount"
case .addAccount:
"addAccount"
case .addTagGroup:
"addTagGroup"
case .addRemoteLocalTimeline:
"addRemoteLocalTimeline"
case .statusEditHistory:
"statusEditHistory"
case .report:
"report"
case .shareImage:
"shareImage"
case .editTagGroup:
"editTagGroup"
case .settings, .support, .about, .accountPushNotficationsSettings:
"settings"
case .timelineContentFilter:
"timelineContentFilter"
}
}
}
@MainActor
@Observable public class RouterPath {
public var client: Client?
public var urlHandler: ((URL) -> OpenURLAction.Result)?
public var path: [RouterDestination] = []
public var presentedSheet: SheetDestination?
public init() {}
public func navigate(to: RouterDestination) {
path.append(to)
}
public func handleStatus(status: AnyStatus, url: URL) -> OpenURLAction.Result {
if url.pathComponents.count == 3, url.pathComponents[1] == "tags",
url.host() == status.account.url?.host(),
let tag = url.pathComponents.last
{
// OK this test looks weird but it's
// A 3 component path i.e. ["/", "tags", "tagname"]
// That is on the same host as the person that posted the tag,
// i.e. not a link that matches the pattern but elsewhere on the internet
// In those circumstances, hijack the link and goto the tags page instead
navigate(to: .hashTag(tag: tag, account: nil))
return .handled
} else if let mention = status.mentions.first(where: { $0.url == url }) {
navigate(to: .accountDetail(id: mention.id))
return .handled
} else if let client,
client.isAuth,
client.hasConnection(with: url),
let id = Int(url.lastPathComponent)
{
if !StatusEmbedCache.shared.badStatusesURLs.contains(url) {
if url.absoluteString.contains(client.server) {
navigate(to: .statusDetail(id: String(id)))
} else {
navigate(to: .remoteStatusDetail(url: url))
}
return .handled
}
}
return urlHandler?(url) ?? .systemAction
}
public func handle(url: URL) -> OpenURLAction.Result {
if url.pathComponents.contains(where: { $0 == "tags" }),
let tag = url.pathComponents.last
{
navigate(to: .hashTag(tag: tag, account: nil))
return .handled
} else if url.lastPathComponent.first == "@",
let host = url.host,
!host.hasPrefix("www")
{
let acct = "\(url.lastPathComponent)@\(host)"
Task {
await navigateToAccountFrom(acct: acct, url: url)
}
return .handled
} else if let client,
client.isAuth,
client.hasConnection(with: url),
let id = Int(url.lastPathComponent)
{
if url.absoluteString.contains(client.server) {
navigate(to: .statusDetail(id: String(id)))
} else {
navigate(to: .remoteStatusDetail(url: url))
}
return .handled
}
return urlHandler?(url) ?? .systemAction
}
public func navigateToAccountFrom(acct: String, url: URL) async {
guard let client else { return }
let results: SearchResults? = try? await client.get(endpoint: Search.search(query: acct,
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)
}
}
public func navigateToAccountFrom(url: URL) async {
guard let client else { return }
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)
}
}
}