mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 08:10:59 +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 Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import HTTP
|
import HTTP
|
||||||
|
import Mastodon
|
||||||
|
import MastodonAPI
|
||||||
|
|
||||||
public struct InstanceURLService {
|
public struct InstanceURLService {
|
||||||
private let httpClient: HTTPClient
|
private let httpClient: HTTPClient
|
||||||
private var userDefaultsClient: UserDefaultsClient
|
private var userDefaultsClient: UserDefaultsClient
|
||||||
|
|
||||||
public init(environment: AppEnvironment) {
|
public init(environment: AppEnvironment) {
|
||||||
httpClient = HTTPClient(session: environment.session, decoder: JSONDecoder())
|
httpClient = HTTPClient(session: environment.session, decoder: MastodonDecoder())
|
||||||
userDefaultsClient = UserDefaultsClient(userDefaults: environment.userDefaults)
|
userDefaultsClient = UserDefaultsClient(userDefaults: environment.userDefaults)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension InstanceURLService {
|
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 {
|
func isFiltered(url: URL) -> Bool {
|
||||||
guard let host = url.host else { return true }
|
guard let host = url.host else { return true }
|
||||||
|
|
||||||
|
@ -55,11 +91,8 @@ private struct UpdatedFilterTarget: DecodableTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension InstanceURLService {
|
private extension InstanceURLService {
|
||||||
var filter: BloomFilter<String> {
|
static let httpsPrefix = "https://"
|
||||||
userDefaultsClient.updatedInstanceFilter ?? Self.defaultFilter
|
static let shortestPossibleURLLength = 4
|
||||||
}
|
|
||||||
|
|
||||||
static let updatedFilterUserDefaultsKey = "updatedFilter"
|
|
||||||
// Ugly, but baking this into the compiled app instead of loading the data from the bundle is more secure
|
// Ugly, but baking this into the compiled app instead of loading the data from the bundle is more secure
|
||||||
// swiftlint:disable line_length
|
// swiftlint:disable line_length
|
||||||
static let defaultFilterData = #"{"hashes":["djb232","djb2a32","fnv132","fnv1a32","sdbm32"],"data":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAIAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAgAAAAAQAAAAAABAAACAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAABAAAEAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAIAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAIAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAIAAAQAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAQAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAADAAAAAAAAAAAAA=="}"#
|
static let defaultFilterData = #"{"hashes":["djb232","djb2a32","fnv132","fnv1a32","sdbm32"],"data":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAIAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAgAAAAAQAAAAAABAAACAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAABAAAEAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAIAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAIAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAIAAAQAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAQAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAADAAAAAAAAAAAAA=="}"#
|
||||||
|
@ -68,4 +101,7 @@ private extension InstanceURLService {
|
||||||
// swiftlint:disable force_try
|
// swiftlint:disable force_try
|
||||||
static let defaultFilter = try! JSONDecoder().decode(BloomFilter<String>.self, from: defaultFilterData)
|
static let defaultFilter = try! JSONDecoder().decode(BloomFilter<String>.self, from: defaultFilterData)
|
||||||
// swiftlint:enable force_try
|
// swiftlint:enable force_try
|
||||||
|
var filter: BloomFilter<String> {
|
||||||
|
userDefaultsClient.updatedInstanceFilter ?? Self.defaultFilter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public enum AddIdentityError: Error {
|
public enum AddIdentityError: Error {
|
||||||
|
@ -12,6 +13,8 @@ public final class AddIdentityViewModel: ObservableObject {
|
||||||
@Published public var urlFieldText = ""
|
@Published public var urlFieldText = ""
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
@Published public private(set) var loading = false
|
@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>
|
public let addedIdentityID: AnyPublisher<UUID, Never>
|
||||||
|
|
||||||
private let allIdentitiesService: AllIdentitiesService
|
private let allIdentitiesService: AllIdentitiesService
|
||||||
|
@ -23,6 +26,21 @@ public final class AddIdentityViewModel: ObservableObject {
|
||||||
self.allIdentitiesService = allIdentitiesService
|
self.allIdentitiesService = allIdentitiesService
|
||||||
self.instanceURLService = instanceURLService
|
self.instanceURLService = instanceURLService
|
||||||
addedIdentityID = addedIdentityIDSubject.eraseToAnyPublisher()
|
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 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) {
|
func addIdentity(authenticated: Bool) {
|
||||||
let identityID = UUID()
|
let identityID = UUID()
|
||||||
|
|
||||||
guard let url = Self.url(fieldText: urlFieldText) else {
|
guard let url = InstanceURLService.url(text: urlFieldText) else {
|
||||||
alertItem = AlertItem(error: AddIdentityError.unableToConnectToInstance)
|
alertItem = AlertItem(error: AddIdentityError.unableToConnectToInstance)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -81,27 +86,29 @@ private extension AddIdentityViewModel {
|
||||||
url: url,
|
url: url,
|
||||||
authenticated: authenticated)
|
authenticated: authenticated)
|
||||||
.receive(on: DispatchQueue.main)
|
.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 })
|
.handleEvents(receiveSubscription: { [weak self] _ in self?.loading = true })
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
self.loading = false
|
self.loading = false
|
||||||
|
|
||||||
if case .finished = $0 {
|
switch $0 {
|
||||||
|
case .finished:
|
||||||
self.addedIdentityIDSubject.send(identityID)
|
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 }
|
} receiveValue: { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkIfPublicTimelineAvailable(url: URL) -> AnyPublisher<Bool, Never> {
|
||||||
|
Just(false).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import KingfisherSwiftUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -9,22 +10,46 @@ struct AddIdentityView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
urlTextField
|
Section {
|
||||||
.autocapitalization(.none)
|
TextField("add-identity.instance-url", text: $viewModel.urlFieldText)
|
||||||
.disableAutocorrection(true)
|
.autocapitalization(.none)
|
||||||
.keyboardType(.URL)
|
.disableAutocorrection(true)
|
||||||
Group {
|
.keyboardType(.URL)
|
||||||
if viewModel.loading {
|
Group {
|
||||||
ProgressView()
|
if viewModel.loading {
|
||||||
} else {
|
ProgressView()
|
||||||
Button("add-identity.log-in",
|
} else {
|
||||||
action: viewModel.logInTapped)
|
Button("add-identity.log-in",
|
||||||
Button("add-identity.browse-anonymously", action: viewModel.browseAnonymouslyTapped)
|
action: viewModel.logInTapped)
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
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)
|
.alertItem($viewModel.alertItem)
|
||||||
.onReceive(viewModel.addedIdentityID) { id in
|
.onReceive(viewModel.addedIdentityID) { id in
|
||||||
withAnimation {
|
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 {
|
extension AddIdentityError: LocalizedError {
|
||||||
public var errorDescription: String? {
|
public var errorDescription: String? {
|
||||||
NSLocalizedString("add-identity.unable-to-connect-to-instance", comment: "")
|
NSLocalizedString("add-identity.unable-to-connect-to-instance", comment: "")
|
||||||
|
|
Loading…
Reference in a new issue