mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 08:10:59 +00:00
Webfingering
This commit is contained in:
parent
cf342bf3ae
commit
eab12976cd
7 changed files with 224 additions and 42 deletions
9
Mastodon/Sources/Mastodon/Entities/Results.swift
Normal file
9
Mastodon/Sources/Mastodon/Entities/Results.swift
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Results: Codable {
|
||||
public let accounts: [Account]
|
||||
public let statuses: [Status]
|
||||
public let hashtags: [Tag]
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import HTTP
|
||||
import Mastodon
|
||||
|
||||
public enum ResultsEndpoint {
|
||||
case search(query: String, resolve: Bool)
|
||||
}
|
||||
|
||||
extension ResultsEndpoint: Endpoint {
|
||||
public typealias ResultType = Results
|
||||
|
||||
public var APIVersion: String {
|
||||
switch self {
|
||||
case .search:
|
||||
return "v2"
|
||||
}
|
||||
}
|
||||
|
||||
public var pathComponentsInContext: [String] {
|
||||
switch self {
|
||||
case .search:
|
||||
return ["search"]
|
||||
}
|
||||
}
|
||||
|
||||
public var method: HTTPMethod {
|
||||
switch self {
|
||||
case .search:
|
||||
return .get
|
||||
}
|
||||
}
|
||||
|
||||
public var queryParameters: [String: String]? {
|
||||
switch self {
|
||||
case let .search(query, resolve):
|
||||
var params = ["q": query]
|
||||
|
||||
if resolve {
|
||||
params["resolve"] = String(true)
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@
|
|||
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; };
|
||||
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; };
|
||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46F24F76169001EBDBB /* View+Extensions.swift */; };
|
||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
|
||||
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
||||
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
|
||||
D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -139,6 +140,7 @@
|
|||
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = "<group>"; };
|
||||
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
|
||||
D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebfingerIndicatorView.swift; sourceTree = "<group>"; };
|
||||
D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; };
|
||||
D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
|
@ -288,6 +290,7 @@
|
|||
D0625E55250F086B00502611 /* Status */,
|
||||
D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */,
|
||||
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */,
|
||||
D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -545,6 +548,7 @@
|
|||
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
|
||||
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */,
|
||||
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
|
||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
|
||||
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
||||
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
|
||||
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
|
||||
|
|
|
@ -10,6 +10,8 @@ public enum Navigation {
|
|||
case url(URL)
|
||||
case statusList(StatusListService)
|
||||
case accountStatuses(AccountStatusesService)
|
||||
case webfingerStart
|
||||
case webfingerEnd
|
||||
}
|
||||
|
||||
public struct NavigationService {
|
||||
|
@ -46,8 +48,12 @@ public extension NavigationService {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
if url.shouldWebfinger {
|
||||
return webfinger(url: url)
|
||||
} else {
|
||||
return Just(.url(url)).eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
func contextStatusListService(id: String) -> StatusListService {
|
||||
StatusListService(statusID: id, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
|
@ -88,6 +94,37 @@ private extension NavigationService {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func webfinger(url: URL) -> AnyPublisher<Navigation, Never> {
|
||||
let navigationSubject = PassthroughSubject<Navigation, Never>()
|
||||
|
||||
let request = mastodonAPIClient.request(ResultsEndpoint.search(query: url.absoluteString, resolve: true))
|
||||
.handleEvents(
|
||||
receiveSubscription: { _ in navigationSubject.send(.webfingerStart) },
|
||||
receiveCompletion: { _ in navigationSubject.send(.webfingerEnd) })
|
||||
.map { results -> Navigation in
|
||||
if let tag = results.hashtags.first {
|
||||
return .statusList(
|
||||
StatusListService(
|
||||
timeline: .tag(tag.name),
|
||||
mastodonAPIClient: mastodonAPIClient,
|
||||
contentDatabase: contentDatabase))
|
||||
} else if let account = results.accounts.first {
|
||||
return .accountStatuses(accountStatusesService(id: account.id))
|
||||
} else if let status = results.statuses.first {
|
||||
return .statusList(
|
||||
StatusListService(
|
||||
statusID: status.id,
|
||||
mastodonAPIClient: mastodonAPIClient,
|
||||
contentDatabase: contentDatabase))
|
||||
} else {
|
||||
return .url(url)
|
||||
}
|
||||
}
|
||||
.replaceError(with: .url(url))
|
||||
|
||||
return navigationSubject.merge(with: request).eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
private extension URL {
|
||||
|
|
|
@ -8,6 +8,7 @@ import ViewModels
|
|||
class CollectionViewController: UITableViewController {
|
||||
private let viewModel: CollectionViewModel
|
||||
private let loadingTableFooterView = LoadingTableFooterView()
|
||||
private let webfingerIndicatorView = WebfingerIndicatorView()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var cellHeightCaches = [CGFloat: [CollectionItem: CGFloat]]()
|
||||
private let dataSourceQueue =
|
||||
|
@ -57,49 +58,15 @@ class CollectionViewController: UITableViewController {
|
|||
tableView.cellLayoutMarginsFollowReadableWidth = true
|
||||
tableView.tableFooterView = UIView()
|
||||
|
||||
viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables)
|
||||
view.addSubview(webfingerIndicatorView)
|
||||
webfingerIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
viewModel.collectionItems
|
||||
.sink { [weak self] in self?.update(items: $0) }
|
||||
.store(in: &cancellables)
|
||||
NSLayoutConstraint.activate([
|
||||
webfingerIndicatorView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
|
||||
webfingerIndicatorView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor)
|
||||
])
|
||||
|
||||
viewModel.navigationEvents.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
switch $0 {
|
||||
case let .share(url):
|
||||
self.share(url: url)
|
||||
case let .collectionNavigation(collectionViewModel):
|
||||
self.show(CollectionViewController(viewModel: collectionViewModel), sender: self)
|
||||
case let .urlNavigation(url):
|
||||
self.present(SFSafariViewController(url: url), animated: true)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.loading
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.tableView.tableFooterView = $0 ? self.loadingTableFooterView : UIView()
|
||||
self.sizeTableHeaderFooterViews()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
if let accountsStatusesViewModel = viewModel as? AccountStatusesViewModel {
|
||||
// Initial size is to avoid unsatisfiable constraint warning
|
||||
let accountHeaderView = AccountHeaderView(
|
||||
frame: .init(
|
||||
origin: .zero,
|
||||
size: .init(width: 100, height: 100)))
|
||||
accountHeaderView.viewModel = accountsStatusesViewModel
|
||||
accountsStatusesViewModel.$account.dropFirst().receive(on: DispatchQueue.main).sink { [weak self] _ in
|
||||
accountHeaderView.viewModel = accountsStatusesViewModel
|
||||
self?.sizeTableHeaderFooterViews()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
tableView.tableHeaderView = accountHeaderView
|
||||
}
|
||||
setupViewModelBindings()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -158,6 +125,57 @@ extension CollectionViewController: UITableViewDataSourcePrefetching {
|
|||
}
|
||||
|
||||
private extension CollectionViewController {
|
||||
func setupViewModelBindings() {
|
||||
viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables)
|
||||
|
||||
viewModel.collectionItems
|
||||
.sink { [weak self] in self?.update(items: $0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.navigationEvents.receive(on: DispatchQueue.main).sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch $0 {
|
||||
case let .share(url):
|
||||
self.share(url: url)
|
||||
case let .collectionNavigation(collectionViewModel):
|
||||
self.show(CollectionViewController(viewModel: collectionViewModel), sender: self)
|
||||
case let .urlNavigation(url):
|
||||
self.present(SFSafariViewController(url: url), animated: true)
|
||||
case .webfingerStart:
|
||||
self.webfingerIndicatorView.startAnimating()
|
||||
case .webfingerEnd:
|
||||
self.webfingerIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.loading
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.tableView.tableFooterView = $0 ? self.loadingTableFooterView : UIView()
|
||||
self.sizeTableHeaderFooterViews()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
if let accountsStatusesViewModel = viewModel as? AccountStatusesViewModel {
|
||||
// Initial size is to avoid unsatisfiable constraint warning
|
||||
let accountHeaderView = AccountHeaderView(
|
||||
frame: .init(
|
||||
origin: .zero,
|
||||
size: .init(width: 100, height: 100)))
|
||||
accountHeaderView.viewModel = accountsStatusesViewModel
|
||||
accountsStatusesViewModel.$account.dropFirst().receive(on: DispatchQueue.main).sink { [weak self] _ in
|
||||
accountHeaderView.viewModel = accountsStatusesViewModel
|
||||
self?.sizeTableHeaderFooterViews()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
tableView.tableHeaderView = accountHeaderView
|
||||
}
|
||||
}
|
||||
|
||||
func update(items: [[CollectionItem]]) {
|
||||
var offsetFromNavigationBar: CGFloat?
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ public enum NavigationEvent {
|
|||
case collectionNavigation(CollectionViewModel)
|
||||
case urlNavigation(URL)
|
||||
case share(URL)
|
||||
case webfingerStart
|
||||
case webfingerEnd
|
||||
}
|
||||
|
||||
extension NavigationEvent {
|
||||
|
@ -21,6 +23,10 @@ extension NavigationEvent {
|
|||
self = .collectionNavigation(StatusListViewModel(statusListService: statusListService))
|
||||
case let .accountStatuses(accountStatusesService):
|
||||
self = .collectionNavigation(AccountStatusesViewModel(accountStatusesService: accountStatusesService))
|
||||
case .webfingerStart:
|
||||
self = .webfingerStart
|
||||
case .webfingerEnd:
|
||||
self = .webfingerEnd
|
||||
}
|
||||
case let .accountListNavigation(accountListViewModel):
|
||||
self = .collectionNavigation(accountListViewModel)
|
||||
|
|
61
Views/WebfingerIndicatorView.swift
Normal file
61
Views/WebfingerIndicatorView.swift
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class WebfingerIndicatorView: UIVisualEffectView {
|
||||
private let activityIndicatorView = UIActivityIndicatorView()
|
||||
|
||||
init() {
|
||||
super.init(effect: nil)
|
||||
|
||||
clipsToBounds = true
|
||||
layer.cornerRadius = 8
|
||||
|
||||
contentView.addSubview(activityIndicatorView)
|
||||
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
activityIndicatorView.style = .large
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
trailingAnchor.constraint(
|
||||
equalTo: activityIndicatorView.trailingAnchor, constant: 8),
|
||||
bottomAnchor.constraint(
|
||||
equalTo: activityIndicatorView.bottomAnchor, constant: 8),
|
||||
activityIndicatorView.topAnchor.constraint(
|
||||
equalTo: topAnchor, constant: 8),
|
||||
activityIndicatorView.leadingAnchor.constraint(
|
||||
equalTo: leadingAnchor, constant: 8),
|
||||
activityIndicatorView.centerXAnchor.constraint(
|
||||
equalTo: contentView.safeAreaLayoutGuide.centerXAnchor),
|
||||
activityIndicatorView.centerYAnchor.constraint(
|
||||
equalTo: contentView.safeAreaLayoutGuide.centerYAnchor)
|
||||
])
|
||||
|
||||
isHidden = true
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension WebfingerIndicatorView {
|
||||
func startAnimating() {
|
||||
isHidden = false
|
||||
activityIndicatorView.startAnimating()
|
||||
|
||||
UIView.animate(withDuration: 0.5) {
|
||||
self.effect = UIBlurEffect(style: .systemUltraThinMaterial)
|
||||
}
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
activityIndicatorView.stopAnimating()
|
||||
|
||||
UIView.animate(withDuration: 0.5) {
|
||||
self.effect = nil
|
||||
} completion: { _ in
|
||||
self.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue