New Sign In flow & instances browser

This commit is contained in:
Thomas Ricouard 2022-12-29 14:07:58 +01:00
parent f7704b808d
commit 03a5dd9f54
6 changed files with 229 additions and 64 deletions

View file

@ -10,6 +10,8 @@
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F24EEB729360C330042359D /* Preview Assets.xcassets */; }; 9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F24EEB729360C330042359D /* Preview Assets.xcassets */; };
9F295540292B6C3400E0E81B /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F29553F292B6C3400E0E81B /* Timeline */; }; 9F295540292B6C3400E0E81B /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F29553F292B6C3400E0E81B /* Timeline */; };
9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F5295AE04800DE16D0 /* Tabs.swift */; }; 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 */; }; 9F35DB44294F9A7D00B3281A /* Status in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB43294F9A7D00B3281A /* Status */; };
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4629506F6600B3281A /* NotificationTab.swift */; }; 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4629506F6600B3281A /* NotificationTab.swift */; };
9F35DB4A29506FA100B3281A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB4929506FA100B3281A /* Notifications */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 9F35DB4629506F6600B3281A /* NotificationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTab.swift; sourceTree = "<group>"; };
@ -170,6 +174,8 @@
children = ( children = (
9FAE4ACA293783B000772766 /* SettingsTab.swift */, 9FAE4ACA293783B000772766 /* SettingsTab.swift */,
9FE151A5293C90F900E9683D /* IconSelectorView.swift */, 9FE151A5293C90F900E9683D /* IconSelectorView.swift */,
9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */,
9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */,
); );
path = Settings; path = Settings;
sourceTree = "<group>"; sourceTree = "<group>";
@ -259,6 +265,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */, 9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */,
9F2B92FC295DA94500DE16D0 /* InstanceInfoView.swift in Sources */,
9F35DB4C2952005C00B3281A /* AccountTab.swift in Sources */, 9F35DB4C2952005C00B3281A /* AccountTab.swift in Sources */,
9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */, 9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */,
9FAE4AD32937A0C600772766 /* AppAccountsManager.swift in Sources */, 9FAE4AD32937A0C600772766 /* AppAccountsManager.swift in Sources */,
@ -266,6 +273,7 @@
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */, 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */,
9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */, 9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */,
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */, 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */,
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */, 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */,
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */, 9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */, 9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */,

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -7,15 +7,13 @@ import Models
import DesignSystem import DesignSystem
struct SettingsTabs: View { struct SettingsTabs: View {
@Environment(\.openURL) private var openURL
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var currentInstance: CurrentInstance @EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var appAccountsManager: AppAccountsManager @EnvironmentObject private var appAccountsManager: AppAccountsManager
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@State private var signInInProgress = false @State private var addAccountSheetPresented = false
@State private var signInServer = IceCubesApp.defaultServer
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@ -25,11 +23,6 @@ struct SettingsTabs: View {
themeSection themeSection
instanceSection instanceSection
} }
.onOpenURL(perform: { url in
Task {
await continueSignIn(url: url)
}
})
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
.navigationTitle(Text("Settings")) .navigationTitle(Text("Settings"))
@ -37,10 +30,8 @@ struct SettingsTabs: View {
} }
.task { .task {
if appAccountsManager.currentAccount.oauthToken != nil { if appAccountsManager.currentAccount.oauthToken != nil {
signInInProgress = true
await currentAccount.fetchCurrentAccount() await currentAccount.fetchCurrentAccount()
await currentInstance.fetchCurrentInstance() await currentInstance.fetchCurrentInstance()
signInInProgress = false
} }
} }
} }
@ -60,10 +51,8 @@ struct SettingsTabs: View {
} }
} }
signOutButton signOutButton
} else {
TextField("Mastodon server", text: $signInServer)
signInButton
} }
addAccountButton
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
@ -97,23 +86,7 @@ struct SettingsTabs: View {
@ViewBuilder @ViewBuilder
private var instanceSection: some View { private var instanceSection: some View {
if let instanceData = currentInstance.instance { if let instanceData = currentInstance.instance {
Section("Instance info") { InstanceInfoView(instance: instanceData)
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)
} }
} }
@ -138,18 +111,14 @@ struct SettingsTabs: View {
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
private var signInButton: some View { private var addAccountButton: some View {
Button { Button {
signInInProgress = true addAccountSheetPresented.toggle()
Task {
await signIn()
}
} label: { } label: {
if signInInProgress { Text("Add account")
ProgressView() }
} else { .sheet(isPresented: $addAccountSheetPresented) {
Text("Sign in") AddAccountView()
}
} }
} }
@ -159,28 +128,5 @@ struct SettingsTabs: View {
} label: { } label: {
Text("Sign out").foregroundColor(.red) 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
}
} }
} }

View file

@ -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?
}

View file

@ -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 []
}
}
}