mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-05 16:29:33 +00:00
Show instance info if available
This commit is contained in:
parent
83bce93225
commit
5586011eea
3 changed files with 113 additions and 51 deletions
|
@ -4,18 +4,54 @@ import CodableBloomFilter
|
|||
import Combine
|
||||
import Foundation
|
||||
import HTTP
|
||||
import Mastodon
|
||||
import MastodonAPI
|
||||
|
||||
public struct InstanceURLService {
|
||||
private let httpClient: HTTPClient
|
||||
private var userDefaultsClient: UserDefaultsClient
|
||||
|
||||
public init(environment: AppEnvironment) {
|
||||
httpClient = HTTPClient(session: environment.session, decoder: JSONDecoder())
|
||||
httpClient = HTTPClient(session: environment.session, decoder: MastodonDecoder())
|
||||
userDefaultsClient = UserDefaultsClient(userDefaults: environment.userDefaults)
|
||||
}
|
||||
}
|
||||
|
||||
public extension InstanceURLService {
|
||||
static func url(text: String) -> URL? {
|
||||
guard text.count >= shortestPossibleURLLength else { return nil }
|
||||
|
||||
if text.hasPrefix(httpsPrefix), let prefixedURL = URL(string: text) {
|
||||
return prefixedURL
|
||||
} else if let unprefixedURL = URL(string: httpsPrefix + text) {
|
||||
return unprefixedURL
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func instance(url: URL) -> AnyPublisher<Instance?, Never> {
|
||||
httpClient.request(
|
||||
MastodonAPITarget(
|
||||
baseURL: url,
|
||||
endpoint: InstanceEndpoint.instance,
|
||||
accessToken: nil))
|
||||
.map { $0 as Instance? }
|
||||
.catch { _ in Just(nil) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func isPublicTimelineAvailable(url: URL) -> AnyPublisher<Bool, Never> {
|
||||
httpClient.request(
|
||||
MastodonAPITarget(
|
||||
baseURL: url,
|
||||
endpoint: TimelinesEndpoint.public(local: true),
|
||||
accessToken: nil))
|
||||
.map { _ in true }
|
||||
.catch { _ in Just(false) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func isFiltered(url: URL) -> Bool {
|
||||
guard let host = url.host else { return true }
|
||||
|
||||
|
@ -55,11 +91,8 @@ private struct UpdatedFilterTarget: DecodableTarget {
|
|||
}
|
||||
|
||||
private extension InstanceURLService {
|
||||
var filter: BloomFilter<String> {
|
||||
userDefaultsClient.updatedInstanceFilter ?? Self.defaultFilter
|
||||
}
|
||||
|
||||
static let updatedFilterUserDefaultsKey = "updatedFilter"
|
||||
static let httpsPrefix = "https://"
|
||||
static let shortestPossibleURLLength = 4
|
||||
// Ugly, but baking this into the compiled app instead of loading the data from the bundle is more secure
|
||||
// swiftlint:disable line_length
|
||||
static let defaultFilterData = #"{"hashes":["djb232","djb2a32","fnv132","fnv1a32","sdbm32"],"data":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAIAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAgAAAAAQAAAAAABAAACAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAABAAAEAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAIAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAIAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAIAAAQAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAQAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAADAAAAAAAAAAAAA=="}"#
|
||||
|
@ -68,4 +101,7 @@ private extension InstanceURLService {
|
|||
// swiftlint:disable force_try
|
||||
static let defaultFilter = try! JSONDecoder().decode(BloomFilter<String>.self, from: defaultFilterData)
|
||||
// swiftlint:enable force_try
|
||||
var filter: BloomFilter<String> {
|
||||
userDefaultsClient.updatedInstanceFilter ?? Self.defaultFilter
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import Combine
|
||||
import Foundation
|
||||
import Mastodon
|
||||
import ServiceLayer
|
||||
|
||||
public enum AddIdentityError: Error {
|
||||
|
@ -12,6 +13,8 @@ public final class AddIdentityViewModel: ObservableObject {
|
|||
@Published public var urlFieldText = ""
|
||||
@Published public var alertItem: AlertItem?
|
||||
@Published public private(set) var loading = false
|
||||
@Published public private(set) var instance: Instance?
|
||||
@Published public private(set) var isPublicTimelineAvailable = false
|
||||
public let addedIdentityID: AnyPublisher<UUID, Never>
|
||||
|
||||
private let allIdentitiesService: AllIdentitiesService
|
||||
|
@ -23,6 +26,21 @@ public final class AddIdentityViewModel: ObservableObject {
|
|||
self.allIdentitiesService = allIdentitiesService
|
||||
self.instanceURLService = instanceURLService
|
||||
addedIdentityID = addedIdentityIDSubject.eraseToAnyPublisher()
|
||||
|
||||
let url = $urlFieldText
|
||||
.debounce(for: 0.5, scheduler: DispatchQueue.main)
|
||||
.removeDuplicates()
|
||||
.compactMap(InstanceURLService.url(text:))
|
||||
.filter { !instanceURLService.isFiltered(url: $0) }
|
||||
.share()
|
||||
|
||||
url.flatMap(instanceURLService.instance(url:))
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: &$instance)
|
||||
|
||||
url.flatMap(instanceURLService.isPublicTimelineAvailable(url:))
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: &$isPublicTimelineAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,23 +61,10 @@ public extension AddIdentityViewModel {
|
|||
}
|
||||
|
||||
private extension AddIdentityViewModel {
|
||||
private static let filteredURL = URL(string: "https://filtered")!
|
||||
private static let HTTPSPrefix = "https://"
|
||||
|
||||
static func url(fieldText: String) -> URL? {
|
||||
if fieldText.hasPrefix(HTTPSPrefix), let prefixedURL = URL(string: fieldText) {
|
||||
return prefixedURL
|
||||
} else if let unprefixedURL = URL(string: HTTPSPrefix + fieldText) {
|
||||
return unprefixedURL
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addIdentity(authenticated: Bool) {
|
||||
let identityID = UUID()
|
||||
|
||||
guard let url = Self.url(fieldText: urlFieldText) else {
|
||||
guard let url = InstanceURLService.url(text: urlFieldText) else {
|
||||
alertItem = AlertItem(error: AddIdentityError.unableToConnectToInstance)
|
||||
|
||||
return
|
||||
|
@ -81,27 +86,29 @@ private extension AddIdentityViewModel {
|
|||
url: url,
|
||||
authenticated: authenticated)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.catch { [weak self] error -> Empty<Never, Never> in
|
||||
if case AuthenticationError.canceled = error {
|
||||
// no-op
|
||||
} else {
|
||||
let displayedError = error is URLError ? AddIdentityError.unableToConnectToInstance : error
|
||||
|
||||
self?.alertItem = AlertItem(error: displayedError)
|
||||
}
|
||||
|
||||
return Empty()
|
||||
}
|
||||
.handleEvents(receiveSubscription: { [weak self] _ in self?.loading = true })
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.loading = false
|
||||
|
||||
if case .finished = $0 {
|
||||
switch $0 {
|
||||
case .finished:
|
||||
self.addedIdentityIDSubject.send(identityID)
|
||||
case let .failure(error):
|
||||
if case AuthenticationError.canceled = error {
|
||||
return
|
||||
}
|
||||
|
||||
let displayedError = error is URLError ? AddIdentityError.unableToConnectToInstance : error
|
||||
|
||||
self.alertItem = AlertItem(error: displayedError)
|
||||
}
|
||||
} receiveValue: { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func checkIfPublicTimelineAvailable(url: URL) -> AnyPublisher<Bool, Never> {
|
||||
Just(false).eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import KingfisherSwiftUI
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
|
@ -9,22 +10,46 @@ struct AddIdentityView: View {
|
|||
|
||||
var body: some View {
|
||||
Form {
|
||||
urlTextField
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.URL)
|
||||
Group {
|
||||
if viewModel.loading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Button("add-identity.log-in",
|
||||
action: viewModel.logInTapped)
|
||||
Button("add-identity.browse-anonymously", action: viewModel.browseAnonymouslyTapped)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
Section {
|
||||
TextField("add-identity.instance-url", text: $viewModel.urlFieldText)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.URL)
|
||||
Group {
|
||||
if viewModel.loading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Button("add-identity.log-in",
|
||||
action: viewModel.logInTapped)
|
||||
if viewModel.isPublicTimelineAvailable {
|
||||
Button("add-identity.browse-anonymously", action: viewModel.browseAnonymouslyTapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
Section {
|
||||
if let instance = viewModel.instance {
|
||||
KFImage(instance.thumbnail)
|
||||
.placeholder {
|
||||
ProgressView()
|
||||
}
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.listRowInsets(EdgeInsets())
|
||||
VStack(alignment: .center) {
|
||||
Text(instance.title)
|
||||
.font(.headline)
|
||||
Text(instance.uri)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowInsets(EdgeInsets())
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.animation(.default)
|
||||
.alertItem($viewModel.alertItem)
|
||||
.onReceive(viewModel.addedIdentityID) { id in
|
||||
withAnimation {
|
||||
|
@ -35,12 +60,6 @@ struct AddIdentityView: View {
|
|||
}
|
||||
}
|
||||
|
||||
extension AddIdentityView {
|
||||
private var urlTextField: some View {
|
||||
TextField("add-identity.instance-url", text: $viewModel.urlFieldText)
|
||||
}
|
||||
}
|
||||
|
||||
extension AddIdentityError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
NSLocalizedString("add-identity.unable-to-connect-to-instance", comment: "")
|
||||
|
|
Loading…
Reference in a new issue