IceCubes/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift

306 lines
9.8 KiB
Swift
Raw Normal View History

2023-01-17 11:36:01 +01:00
import AppAccount
import Combine
2022-12-29 14:07:58 +01:00
import DesignSystem
2023-01-17 11:36:01 +01:00
import Env
import Models
import Network
2022-12-29 14:07:58 +01:00
import NukeUI
2023-01-22 06:38:30 +01:00
import SafariServices
2023-01-17 11:36:01 +01:00
import SwiftUI
2024-06-08 18:21:33 +02:00
import AuthenticationServices
2022-12-29 14:07:58 +01:00
2023-09-18 21:03:52 +02:00
@MainActor
2022-12-29 14:07:58 +01:00
struct AddAccountView: View {
2024-06-08 18:21:33 +02:00
@Environment(\.webAuthenticationSession) private var webAuthenticationSession
2022-12-29 14:07:58 +01:00
@Environment(\.dismiss) private var dismiss
@Environment(\.scenePhase) private var scenePhase
2023-10-26 17:47:19 +02:00
@Environment(\.openURL) private var openURL
2023-01-17 11:36:01 +01:00
@Environment(AppAccountsManager.self) private var appAccountsManager
@Environment(CurrentAccount.self) private var currentAccount
@Environment(CurrentInstance.self) private var currentInstance
@Environment(PushNotificationsService.self) private var pushNotifications
2023-09-18 21:03:52 +02:00
@Environment(Theme.self) private var theme
2023-01-17 11:36:01 +01:00
2022-12-29 14:07:58 +01:00
@State private var instanceName: String = ""
@State private var instance: Instance?
@State private var isSigninIn = false
@State private var signInClient: Client?
@State private var instances: [InstanceSocial] = []
@State private var instanceFetchError: LocalizedStringKey?
@State private var instanceSocialClient = InstanceSocialClient()
@State private var searchingTask = Task<Void, Never> {}
@State private var getInstanceDetailTask = Task<Void, Never> {}
private let instanceNamePublisher = PassthroughSubject<String, Never>()
2023-03-13 13:38:28 +01:00
private var sanitizedName: String {
2023-03-13 13:38:28 +01:00
var name = instanceName
.replacingOccurrences(of: "http://", with: "")
.replacingOccurrences(of: "https://", with: "")
if name.contains("@") {
let parts = name.components(separatedBy: "@")
name = parts[parts.count - 1] // [@]username@server.address.com
}
2023-03-13 13:38:28 +01:00
return name
}
2023-01-17 11:36:01 +01:00
2023-01-01 09:19:00 +01:00
@FocusState private var isInstanceURLFieldFocused: Bool
2023-01-17 11:36:01 +01:00
private func cleanServerStr(_ server: String) -> String {
server.replacingOccurrences(of: " ", with: "")
}
2022-12-29 14:07:58 +01:00
var body: some View {
NavigationStack {
Form {
TextField("instance.url", text: $instanceName)
#if !os(visionOS)
2022-12-29 14:07:58 +01:00
.listRowBackground(theme.primaryBackgroundColor)
#endif
.keyboardType(.URL)
.textContentType(.URL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
2023-01-01 09:19:00 +01:00
.focused($isInstanceURLFieldFocused)
.onChange(of: instanceName) { _, _ in
instanceName = cleanServerStr(instanceName)
}
2023-01-01 09:19:00 +01:00
if let instanceFetchError {
Text(instanceFetchError)
}
2023-01-21 16:54:43 +01:00
if instance != nil || !instanceName.isEmpty {
signInSection
2023-01-21 16:54:43 +01:00
}
if let instance {
2023-01-07 18:12:56 +01:00
InstanceInfoSection(instance: instance)
2022-12-29 14:07:58 +01:00
} else {
instancesListView
}
}
.formStyle(.grouped)
.navigationTitle("account.add.navigation-title")
2022-12-29 14:07:58 +01:00
.navigationBarTitleDisplayMode(.inline)
#if !os(visionOS)
2024-02-14 12:48:14 +01:00
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
#endif
2024-02-14 12:48:14 +01:00
.toolbar {
if !appAccountsManager.availableAccounts.isEmpty {
CancelToolbarItem()
}
2022-12-29 14:07:58 +01:00
}
2024-02-14 12:48:14 +01:00
.onAppear {
isInstanceURLFieldFocused = true
Task {
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation {
self.instances = instances
}
2023-01-21 16:54:43 +01:00
}
2024-02-14 12:48:14 +01:00
isSigninIn = false
2022-12-29 14:07:58 +01:00
}
2024-02-14 12:48:14 +01:00
.onChange(of: instanceName) {
searchingTask.cancel()
searchingTask = Task {
try? await Task.sleep(for: .seconds(0.1))
guard !Task.isCancelled else { return }
2024-02-14 12:48:14 +01:00
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation {
self.instances = instances
}
}
2024-02-14 12:48:14 +01:00
getInstanceDetailTask.cancel()
getInstanceDetailTask = Task {
try? await Task.sleep(for: .seconds(0.1))
guard !Task.isCancelled else { return }
2024-02-14 12:48:14 +01:00
do {
// bare bones preflight for domain validity
let instanceDetailClient = Client(server: sanitizedName)
if
instanceDetailClient.server.contains("."),
instanceDetailClient.server.last != "."
{
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
withAnimation {
self.instance = instance
instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
}
instanceFetchError = nil
} else {
instance = nil
instanceFetchError = nil
}
2024-02-14 12:48:14 +01:00
} catch _ as DecodingError {
instance = nil
instanceFetchError = "account.add.error.instance-not-supported"
} catch {
instance = nil
2023-01-21 16:54:43 +01:00
}
2022-12-29 14:07:58 +01:00
}
}
2024-02-14 12:48:14 +01:00
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:
isSigninIn = false
default:
break
}
}
2022-12-29 14:07:58 +01:00
}
}
2023-01-17 11:36:01 +01:00
private var signInSection: some View {
Section {
Button {
2023-01-21 16:54:43 +01:00
withAnimation {
isSigninIn = true
}
Task {
await signIn()
}
} label: {
HStack {
Spacer()
if isSigninIn || !sanitizedName.isEmpty && instance == nil {
ProgressView()
.id(sanitizedName)
.tint(theme.labelColor)
} else {
Text("account.add.sign-in")
.font(.scaledHeadline)
}
Spacer()
}
}
.buttonStyle(.borderedProminent)
}
#if !os(visionOS)
.listRowBackground(theme.tintColor)
#endif
}
2023-01-17 11:36:01 +01:00
2022-12-29 14:07:58 +01:00
private var instancesListView: some View {
Section("instance.suggestions") {
2022-12-29 14:07:58 +01:00
if instances.isEmpty {
placeholderRow
2022-12-29 14:07:58 +01:00
} else {
ForEach(instances) { instance in
Button {
2023-09-16 14:15:03 +02:00
instanceName = instance.name
} label: {
VStack(alignment: .leading, spacing: 4) {
LazyImage(url: instance.thumbnail) { state in
if let image = state.image {
image
.resizable()
.scaledToFill()
} else {
Rectangle().fill(theme.tintColor.opacity(0.1))
}
}
.frame(height: 100)
.frame(maxWidth: .infinity)
.clipped()
VStack(alignment: .leading) {
HStack {
Text(instance.name)
.font(.scaledHeadline)
.foregroundColor(.primary)
Spacer()
(Text("instance.list.users-\(formatAsNumber(instance.users))")
2024-02-14 12:48:14 +01:00
+ Text("")
+ Text("instance.list.posts-\(formatAsNumber(instance.statuses))"))
.foregroundStyle(theme.tintColor)
}
.padding(.bottom, 5)
Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "")
.foregroundStyle(Color.secondary)
.lineLimit(10)
}
.font(.scaledFootnote)
.padding(10)
}
2022-12-29 14:07:58 +01:00
}
#if !os(visionOS)
.background(theme.primaryBackgroundColor)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
.listRowSeparator(.hidden)
.clipShape(RoundedRectangle(cornerRadius: 4))
#endif
2022-12-29 14:07:58 +01:00
}
}
}
}
2023-01-17 11:36:01 +01:00
private func formatAsNumber(_ string: String) -> String {
(Int(string) ?? 0)
.formatted(
.number
.notation(.compactName)
.locale(.current)
)
}
private var placeholderRow: some View {
VStack(alignment: .leading, spacing: 4) {
Text("placeholder.loading.short")
.font(.scaledHeadline)
.foregroundColor(.primary)
Text("placeholder.loading.long")
.font(.scaledBody)
2023-12-04 15:49:44 +01:00
.foregroundStyle(.secondary)
Text("placeholder.loading.short")
.font(.scaledFootnote)
2023-12-04 15:49:44 +01:00
.foregroundStyle(.secondary)
}
.redacted(reason: .placeholder)
2023-09-18 18:55:11 +02:00
.allowsHitTesting(false)
#if !os(visionOS)
2024-02-14 12:48:14 +01:00
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
2023-01-17 11:36:01 +01:00
2022-12-29 14:07:58 +01:00
private func signIn() async {
2023-12-07 09:45:34 +01:00
signInClient = .init(server: sanitizedName)
if let oauthURL = try? await signInClient?.oauthURL(),
let url = try? await webAuthenticationSession.authenticate(using: oauthURL,
2023-12-18 08:22:59 +01:00
callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: ""))
{
2023-12-07 09:45:34 +01:00
await continueSignIn(url: url)
} else {
2022-12-29 14:07:58 +01:00
isSigninIn = false
}
}
2023-01-17 11:36:01 +01:00
2022-12-29 14:07:58 +01:00
private func continueSignIn(url: URL) async {
guard let client = signInClient else {
isSigninIn = false
return
}
do {
let oauthToken = try await client.continueOauthFlow(url: url)
let client = Client(server: client.server, oauthToken: oauthToken)
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
appAccountsManager.add(account: AppAccount(server: client.server,
accountName: "\(account.acct)@\(client.server)",
oauthToken: oauthToken))
2023-01-08 10:22:52 +01:00
Task {
pushNotifications.setAccounts(accounts: appAccountsManager.pushAccounts)
await pushNotifications.updateSubscriptions(forceCreate: true)
2023-01-08 10:22:52 +01:00
}
2022-12-29 14:07:58 +01:00
isSigninIn = false
dismiss()
} catch {
isSigninIn = false
}
}
}