mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
URL navigation
This commit is contained in:
parent
a595133d70
commit
d7c99a08a8
8 changed files with 171 additions and 56 deletions
|
@ -1,42 +0,0 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
var isAccountURL: Bool {
|
||||
(pathComponents.count == 2 && pathComponents[1].starts(with: "@"))
|
||||
|| (pathComponents.count == 3 && pathComponents[0...1] == ["/", "users"])
|
||||
}
|
||||
|
||||
var accountID: String? {
|
||||
if let accountID = pathComponents.last, pathComponents == ["/", "web", "accounts", accountID] {
|
||||
return accountID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var statusID: String? {
|
||||
guard let statusID = pathComponents.last else { return nil }
|
||||
|
||||
if pathComponents.count == 3, pathComponents[1].starts(with: "@") {
|
||||
return statusID
|
||||
} else if pathComponents == ["/", "web", "statuses", statusID] {
|
||||
return statusID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var tag: String? {
|
||||
if let tag = pathComponents.last, pathComponents == ["/", "tags", tag] {
|
||||
return tag
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var shouldWebfinger: Bool {
|
||||
isAccountURL || accountID != nil || statusID != nil || tag != nil
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ public extension StatusListService {
|
|||
}
|
||||
|
||||
func statusService(status: Status) -> StatusService {
|
||||
StatusService(status: status, networkClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
StatusService(status: status, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
|
||||
func contextService(statusID: String) -> Self {
|
||||
|
|
|
@ -8,12 +8,17 @@ import MastodonAPI
|
|||
|
||||
public struct StatusService {
|
||||
public let status: Status
|
||||
public let urlService: URLService
|
||||
private let mastodonAPIClient: MastodonAPIClient
|
||||
private let contentDatabase: ContentDatabase
|
||||
|
||||
init(status: Status, networkClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||
init(status: Status, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||
self.status = status
|
||||
self.mastodonAPIClient = networkClient
|
||||
self.urlService = URLService(
|
||||
status: status.displayStatus,
|
||||
mastodonAPIClient: mastodonAPIClient,
|
||||
contentDatabase: contentDatabase)
|
||||
self.mastodonAPIClient = mastodonAPIClient
|
||||
self.contentDatabase = contentDatabase
|
||||
}
|
||||
}
|
||||
|
|
103
ServiceLayer/Sources/ServiceLayer/Services/URLService.swift
Normal file
103
ServiceLayer/Sources/ServiceLayer/Services/URLService.swift
Normal file
|
@ -0,0 +1,103 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import DB
|
||||
import Foundation
|
||||
import Mastodon
|
||||
import MastodonAPI
|
||||
|
||||
public enum URLItem {
|
||||
case url(URL)
|
||||
case statusID(String)
|
||||
case accountID(String)
|
||||
case tag(String)
|
||||
}
|
||||
|
||||
public struct URLService {
|
||||
private let status: Status?
|
||||
private let mastodonAPIClient: MastodonAPIClient
|
||||
private let contentDatabase: ContentDatabase
|
||||
|
||||
init(status: Status?, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||
self.status = status
|
||||
self.mastodonAPIClient = mastodonAPIClient
|
||||
self.contentDatabase = contentDatabase
|
||||
}
|
||||
}
|
||||
|
||||
public extension URLService {
|
||||
func item(url: URL) -> AnyPublisher<URLItem, Never> {
|
||||
if let tag = tag(url: url) {
|
||||
return Just(.tag(tag)).eraseToAnyPublisher()
|
||||
} else if let accountID = accountID(url: url) {
|
||||
return Just(.accountID(accountID)).eraseToAnyPublisher()
|
||||
} else if mastodonAPIClient.instanceURL.host == url.host, let statusID = url.statusID {
|
||||
return Just(.statusID(statusID)).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Just(.url(url)).eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
private extension URLService {
|
||||
func tag(url: URL) -> String? {
|
||||
if status?.tags.first(where: { $0.url.path.lowercased() == url.path.lowercased() }) != nil {
|
||||
return url.lastPathComponent
|
||||
} else if
|
||||
mastodonAPIClient.instanceURL.host == url.host {
|
||||
return url.tag
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func accountID(url: URL) -> String? {
|
||||
if let mentionID = status?.mentions.first(where: { $0.url.path.lowercased() == url.path.lowercased() })?.id {
|
||||
return mentionID
|
||||
} else if
|
||||
mastodonAPIClient.instanceURL.host == url.host {
|
||||
return url.accountID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private extension URL {
|
||||
var isAccountURL: Bool {
|
||||
(pathComponents.count == 2 && pathComponents[1].starts(with: "@"))
|
||||
|| (pathComponents.count == 3 && pathComponents[0...1] == ["/", "users"])
|
||||
}
|
||||
|
||||
var accountID: String? {
|
||||
if let accountID = pathComponents.last, pathComponents == ["/", "web", "accounts", accountID] {
|
||||
return accountID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var statusID: String? {
|
||||
guard let statusID = pathComponents.last else { return nil }
|
||||
|
||||
if pathComponents.count == 3, pathComponents[1].starts(with: "@") {
|
||||
return statusID
|
||||
} else if pathComponents == ["/", "web", "statuses", statusID] {
|
||||
return statusID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var tag: String? {
|
||||
if let tag = pathComponents.last, pathComponents == ["/", "tags", tag] {
|
||||
return tag
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var shouldWebfinger: Bool {
|
||||
isAccountURL || accountID != nil || statusID != nil || tag != nil
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import SafariServices
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
|
@ -78,11 +79,15 @@ final class StatusListViewController: UITableViewController {
|
|||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.statusEvents.sink { [weak self] in
|
||||
viewModel.events.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
switch $0 {
|
||||
case .ignorableOutput, .statusListNavigation, .urlNavigation: break
|
||||
case let .share(url):
|
||||
self?.share(url: url)
|
||||
self.share(url: url)
|
||||
case let .statusListNavigation(statusListViewModel):
|
||||
self.show(StatusListViewController(viewModel: statusListViewModel), sender: self)
|
||||
case let .urlNavigation(url):
|
||||
self.present(SFSafariViewController(url: url), animated: true)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
|
|
@ -9,18 +9,18 @@ public final class StatusListViewModel: ObservableObject {
|
|||
@Published public private(set) var statusIDs = [[String]]()
|
||||
@Published public var alertItem: AlertItem?
|
||||
@Published public private(set) var loading = false
|
||||
public let statusEvents: AnyPublisher<StatusViewModel.Event, Never>
|
||||
public let events: AnyPublisher<Event, Never>
|
||||
public private(set) var maintainScrollPositionOfStatusID: String?
|
||||
|
||||
private var statuses = [String: Status]()
|
||||
private let statusListService: StatusListService
|
||||
private var statusViewModelCache = [Status: (StatusViewModel, AnyCancellable)]()
|
||||
private let statusEventsSubject = PassthroughSubject<StatusViewModel.Event, Never>()
|
||||
private let eventsSubject = PassthroughSubject<Event, Never>()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(statusListService: StatusListService) {
|
||||
self.statusListService = statusListService
|
||||
statusEvents = statusEventsSubject.eraseToAnyPublisher()
|
||||
events = eventsSubject.eraseToAnyPublisher()
|
||||
|
||||
statusListService.statusSections
|
||||
.combineLatest(statusListService.filters.map { $0.regularExpression() })
|
||||
|
@ -37,6 +37,14 @@ public final class StatusListViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public extension StatusListViewModel {
|
||||
enum Event {
|
||||
case statusListNavigation(StatusListViewModel)
|
||||
case urlNavigation(URL)
|
||||
case share(URL)
|
||||
}
|
||||
}
|
||||
|
||||
public extension StatusListViewModel {
|
||||
var paginates: Bool { statusListService.paginates }
|
||||
|
||||
|
@ -57,7 +65,7 @@ public extension StatusListViewModel {
|
|||
guard let status = statuses[id] else { return nil }
|
||||
|
||||
var statusViewModel: StatusViewModel
|
||||
|
||||
|
||||
if let cachedViewModel = statusViewModelCache[status]?.0 {
|
||||
statusViewModel = cachedViewModel
|
||||
} else {
|
||||
|
@ -66,7 +74,12 @@ public extension StatusListViewModel {
|
|||
statusViewModel.events
|
||||
.flatMap { $0 }
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { [weak self] in self?.statusEventsSubject.send($0) })
|
||||
.sink { [weak self] in
|
||||
guard let self = self,
|
||||
let event = self.event(statusEvent: $0)
|
||||
else { return }
|
||||
self.eventsSubject.send(event)
|
||||
})
|
||||
}
|
||||
|
||||
statusViewModel.isContextParent = status.id == statusListService.contextParentID
|
||||
|
@ -93,6 +106,28 @@ private extension StatusListViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
func event(statusEvent: StatusViewModel.Event) -> Event? {
|
||||
switch statusEvent {
|
||||
case .ignorableOutput:
|
||||
return nil
|
||||
case let .navigation(item):
|
||||
switch item {
|
||||
case let .url(url):
|
||||
return .urlNavigation(url)
|
||||
case let .accountID(id):
|
||||
return nil
|
||||
case let .statusID(id):
|
||||
return .statusListNavigation(
|
||||
StatusListViewModel(
|
||||
statusListService: statusListService.contextService(statusID: id)))
|
||||
case let .tag(tag):
|
||||
return nil
|
||||
}
|
||||
case let .share(url):
|
||||
return .share(url)
|
||||
}
|
||||
}
|
||||
|
||||
func determineIfScrollPositionShouldBeMaintained(newStatusSections: [[Status]]) {
|
||||
maintainScrollPositionOfStatusID = nil // clear old value
|
||||
|
||||
|
|
|
@ -52,8 +52,7 @@ public struct StatusViewModel {
|
|||
public extension StatusViewModel {
|
||||
enum Event {
|
||||
case ignorableOutput
|
||||
case statusListNavigation(StatusListViewModel)
|
||||
case urlNavigation(URL)
|
||||
case navigation(URLItem)
|
||||
case share(URL)
|
||||
}
|
||||
}
|
||||
|
@ -116,6 +115,14 @@ public extension StatusViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
func urlSelected(_ url: URL) {
|
||||
eventsSubject.send(
|
||||
statusService.urlService.item(url: url)
|
||||
.map { Event.navigation($0) }
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func toggleFavorited() {
|
||||
eventsSubject.send(statusService.toggleFavorited().map { _ in Event.ignorableOutput }.eraseToAnyPublisher())
|
||||
}
|
||||
|
|
|
@ -99,7 +99,9 @@ extension StatusView: UITextViewDelegate {
|
|||
in characterRange: NSRange,
|
||||
interaction: UITextItemInteraction) -> Bool {
|
||||
switch interaction {
|
||||
case .invokeDefaultAction: print(URL); return false
|
||||
case .invokeDefaultAction:
|
||||
statusConfiguration.viewModel.urlSelected(URL)
|
||||
return false
|
||||
case .preview: return false
|
||||
case .presentActions: return false
|
||||
@unknown default: return false
|
||||
|
|
Loading…
Reference in a new issue