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 */; };
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 */,

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

@ -6,16 +6,14 @@ import Account
import Models
import DesignSystem
struct SettingsTabs: View {
@Environment(\.openURL) private var openURL
struct SettingsTabs: View {
@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
}
}
}

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