New Sign In flow & instances browser
This commit is contained in:
parent
f7704b808d
commit
03a5dd9f54
|
@ -10,6 +10,8 @@
|
|||
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F24EEB729360C330042359D /* Preview Assets.xcassets */; };
|
||||
9F295540292B6C3400E0E81B /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F29553F292B6C3400E0E81B /* Timeline */; };
|
||||
9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F5295AE04800DE16D0 /* Tabs.swift */; };
|
||||
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */; };
|
||||
9F2B92FC295DA94500DE16D0 /* InstanceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */; };
|
||||
9F35DB44294F9A7D00B3281A /* Status in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB43294F9A7D00B3281A /* Status */; };
|
||||
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4629506F6600B3281A /* NotificationTab.swift */; };
|
||||
9F35DB4A29506FA100B3281A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB4929506FA100B3281A /* Notifications */; };
|
||||
|
@ -36,6 +38,8 @@
|
|||
9F29553D292B67B600E0E81B /* Network */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Network; path = Packages/Network; sourceTree = "<group>"; };
|
||||
9F29553E292B6AF600E0E81B /* Timeline */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Timeline; path = Packages/Timeline; sourceTree = "<group>"; };
|
||||
9F2B92F5295AE04800DE16D0 /* Tabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tabs.swift; sourceTree = "<group>"; };
|
||||
9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountsView.swift; sourceTree = "<group>"; };
|
||||
9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceInfoView.swift; sourceTree = "<group>"; };
|
||||
9F35DB42294F9A2900B3281A /* Status */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Status; path = Packages/Status; sourceTree = "<group>"; };
|
||||
9F35DB45294FA04C00B3281A /* DesignSystem */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DesignSystem; path = Packages/DesignSystem; sourceTree = "<group>"; };
|
||||
9F35DB4629506F6600B3281A /* NotificationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTab.swift; sourceTree = "<group>"; };
|
||||
|
@ -170,6 +174,8 @@
|
|||
children = (
|
||||
9FAE4ACA293783B000772766 /* SettingsTab.swift */,
|
||||
9FE151A5293C90F900E9683D /* IconSelectorView.swift */,
|
||||
9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */,
|
||||
9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
|
@ -259,6 +265,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */,
|
||||
9F2B92FC295DA94500DE16D0 /* InstanceInfoView.swift in Sources */,
|
||||
9F35DB4C2952005C00B3281A /* AccountTab.swift in Sources */,
|
||||
9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */,
|
||||
9FAE4AD32937A0C600772766 /* AppAccountsManager.swift in Sources */,
|
||||
|
@ -266,6 +273,7 @@
|
|||
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */,
|
||||
9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */,
|
||||
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
|
||||
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */,
|
||||
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */,
|
||||
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
|
||||
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
import SwiftUI
|
||||
import Network
|
||||
import Models
|
||||
import Env
|
||||
import DesignSystem
|
||||
import NukeUI
|
||||
import Shimmer
|
||||
|
||||
struct AddAccountView: View {
|
||||
@Environment(\.openURL) private var openURL
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@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] = []
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
TextField("Instance url", text: $instanceName)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
if let instance {
|
||||
Button {
|
||||
isSigninIn = true
|
||||
Task {
|
||||
await signIn()
|
||||
}
|
||||
} label: {
|
||||
if isSigninIn {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("Sign in")
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
InstanceInfoView(instance: instance)
|
||||
} else {
|
||||
instancesListView
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle("Add account")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel", action: { dismiss() })
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
let client = InstanceSocialClient()
|
||||
Task {
|
||||
self.instances = await client.fetchInstances()
|
||||
}
|
||||
}
|
||||
.onChange(of: instanceName) { newValue in
|
||||
let client = Client(server: newValue)
|
||||
Task {
|
||||
do {
|
||||
self.instance = try await client.get(endpoint: Instances.instance)
|
||||
} catch {
|
||||
self.instance = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.onOpenURL(perform: { url in
|
||||
Task {
|
||||
await continueSignIn(url: url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private var instancesListView: some View {
|
||||
Section("Suggestions") {
|
||||
if instances.isEmpty {
|
||||
ProgressView()
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
} else {
|
||||
ForEach(instanceName.isEmpty ? instances : instances.filter{ $0.name.contains(instanceName.lowercased()) }) { instance in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(instance.name)
|
||||
.font(.headline)
|
||||
Text(instance.info?.shortDescription ?? "")
|
||||
.font(.body)
|
||||
.foregroundColor(.gray)
|
||||
Text("\(instance.users) users ⸱ \(instance.statuses) posts")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.onTapGesture {
|
||||
self.instanceName = instance.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func signIn() async {
|
||||
do {
|
||||
signInClient = .init(server: instanceName)
|
||||
if let oauthURL = try await signInClient?.oauthURL() {
|
||||
openURL(oauthURL)
|
||||
} else {
|
||||
isSigninIn = false
|
||||
}
|
||||
} catch {
|
||||
isSigninIn = false
|
||||
}
|
||||
}
|
||||
|
||||
private func continueSignIn(url: URL) async {
|
||||
guard let client = signInClient else {
|
||||
isSigninIn = false
|
||||
return
|
||||
}
|
||||
do {
|
||||
let oauthToken = try await client.continueOauthFlow(url: url)
|
||||
appAccountsManager.add(account: AppAccount(server: client.server, oauthToken: oauthToken))
|
||||
await currentAccount.fetchCurrentAccount()
|
||||
await currentInstance.fetchCurrentInstance()
|
||||
isSigninIn = false
|
||||
dismiss()
|
||||
} catch {
|
||||
isSigninIn = false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import DesignSystem
|
||||
import NukeUI
|
||||
|
||||
struct InstanceInfoView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
let instance: Instance
|
||||
|
||||
var body: some View {
|
||||
Section("Instance info") {
|
||||
LabeledContent("Name", value: instance.title)
|
||||
Text(instance.shortDescription)
|
||||
LabeledContent("Email", value: instance.email)
|
||||
LabeledContent("Version", value: instance.version)
|
||||
LabeledContent("Users", value: "\(instance.stats.userCount)")
|
||||
LabeledContent("Posts", value: "\(instance.stats.statusCount)")
|
||||
LabeledContent("Domains", value: "\(instance.stats.domainCount)")
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
Section("Instance rules") {
|
||||
ForEach(instance.rules) { rule in
|
||||
Text(rule.text)
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
}
|
|
@ -7,15 +7,13 @@ import Models
|
|||
import DesignSystem
|
||||
|
||||
struct SettingsTabs: View {
|
||||
@Environment(\.openURL) private var openURL
|
||||
@EnvironmentObject private var client: Client
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@State private var signInInProgress = false
|
||||
@State private var signInServer = IceCubesApp.defaultServer
|
||||
@State private var addAccountSheetPresented = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
@ -25,11 +23,6 @@ struct SettingsTabs: View {
|
|||
themeSection
|
||||
instanceSection
|
||||
}
|
||||
.onOpenURL(perform: { url in
|
||||
Task {
|
||||
await continueSignIn(url: url)
|
||||
}
|
||||
})
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.navigationTitle(Text("Settings"))
|
||||
|
@ -37,10 +30,8 @@ struct SettingsTabs: View {
|
|||
}
|
||||
.task {
|
||||
if appAccountsManager.currentAccount.oauthToken != nil {
|
||||
signInInProgress = true
|
||||
await currentAccount.fetchCurrentAccount()
|
||||
await currentInstance.fetchCurrentInstance()
|
||||
signInInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,10 +51,8 @@ struct SettingsTabs: View {
|
|||
}
|
||||
}
|
||||
signOutButton
|
||||
} else {
|
||||
TextField("Mastodon server", text: $signInServer)
|
||||
signInButton
|
||||
}
|
||||
addAccountButton
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
@ -97,23 +86,7 @@ struct SettingsTabs: View {
|
|||
@ViewBuilder
|
||||
private var instanceSection: some View {
|
||||
if let instanceData = currentInstance.instance {
|
||||
Section("Instance info") {
|
||||
LabeledContent("Name", value: instanceData.title)
|
||||
Text(instanceData.shortDescription)
|
||||
LabeledContent("Email", value: instanceData.email)
|
||||
LabeledContent("Version", value: instanceData.version)
|
||||
LabeledContent("Users", value: "\(instanceData.stats.userCount)")
|
||||
LabeledContent("Posts", value: "\(instanceData.stats.statusCount)")
|
||||
LabeledContent("Domains", value: "\(instanceData.stats.domainCount)")
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
Section("Instance rules") {
|
||||
ForEach(instanceData.rules) { rule in
|
||||
Text(rule.text)
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
InstanceInfoView(instance: instanceData)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,18 +111,14 @@ struct SettingsTabs: View {
|
|||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
private var signInButton: some View {
|
||||
private var addAccountButton: some View {
|
||||
Button {
|
||||
signInInProgress = true
|
||||
Task {
|
||||
await signIn()
|
||||
}
|
||||
addAccountSheetPresented.toggle()
|
||||
} label: {
|
||||
if signInInProgress {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("Sign in")
|
||||
Text("Add account")
|
||||
}
|
||||
.sheet(isPresented: $addAccountSheetPresented) {
|
||||
AddAccountView()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,28 +128,5 @@ struct SettingsTabs: View {
|
|||
} label: {
|
||||
Text("Sign out").foregroundColor(.red)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func signIn() async {
|
||||
do {
|
||||
client.server = signInServer
|
||||
let oauthURL = try await client.oauthURL()
|
||||
openURL(oauthURL)
|
||||
} catch {
|
||||
signInInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
private func continueSignIn(url: URL) async {
|
||||
do {
|
||||
let oauthToken = try await client.continueOauthFlow(url: url)
|
||||
appAccountsManager.add(account: AppAccount(server: client.server, oauthToken: oauthToken))
|
||||
await currentAccount.fetchCurrentAccount()
|
||||
await currentInstance.fetchCurrentInstance()
|
||||
signInInProgress = false
|
||||
} catch {
|
||||
signInInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import Foundation
|
||||
|
||||
public struct InstanceSocial: Decodable, Identifiable {
|
||||
public struct Info: Decodable {
|
||||
public let shortDescription: String
|
||||
}
|
||||
public let id: String
|
||||
public let name: String
|
||||
public let dead: Bool
|
||||
public let users: String
|
||||
public let activeUsers: Int?
|
||||
public let statuses: String
|
||||
public let thumbnail: URL?
|
||||
public let info: Info?
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import Foundation
|
||||
import Models
|
||||
|
||||
public struct InstanceSocialClient {
|
||||
private let authorization = "Bearer 8a4xx3D7Hzu1aFnf18qlkH8oU0oZ5ulabXxoS2FtQtwOy8G0DGQhr5PjTIjBnYAmFrSBuE2CcASjFocxJBonY8XGbLySB7MXd9ssrwlRHUXTQh3Z578lE1OfUtafvhML"
|
||||
private let endpoint = URL(string: "https://instances.social/api/1.0/instances/list?count=1000&include_closed=false&include_dead=false&min_active_users=500")!
|
||||
|
||||
struct Response: Decodable {
|
||||
let instances: [InstanceSocial]
|
||||
}
|
||||
|
||||
public init() {
|
||||
|
||||
}
|
||||
|
||||
public func fetchInstances() async -> [InstanceSocial] {
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
var request: URLRequest = .init(url: endpoint)
|
||||
request.setValue(authorization, forHTTPHeaderField: "Authorization")
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
let response = try decoder.decode(Response.self, from: data)
|
||||
return response.instances
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue