mirror of
https://github.com/metabolist/metatext.git
synced 2025-06-07 08:38:49 +00:00
Pending accounts
This commit is contained in:
parent
9f80ac6360
commit
1b49ec2515
15 changed files with 149 additions and 84 deletions
|
@ -7,6 +7,7 @@ public struct Identity: Codable, Hashable, Identifiable {
|
||||||
public let id: UUID
|
public let id: UUID
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let authenticated: Bool
|
public let authenticated: Bool
|
||||||
|
public let pending: Bool
|
||||||
public let lastUsedAt: Date
|
public let lastUsedAt: Date
|
||||||
public let preferences: Identity.Preferences
|
public let preferences: Identity.Preferences
|
||||||
public let instance: Identity.Instance?
|
public let instance: Identity.Instance?
|
||||||
|
|
|
@ -9,6 +9,7 @@ extension Identity {
|
||||||
id: result.identity.id,
|
id: result.identity.id,
|
||||||
url: result.identity.url,
|
url: result.identity.url,
|
||||||
authenticated: result.identity.authenticated,
|
authenticated: result.identity.authenticated,
|
||||||
|
pending: result.identity.pending,
|
||||||
lastUsedAt: result.identity.lastUsedAt,
|
lastUsedAt: result.identity.lastUsedAt,
|
||||||
preferences: result.identity.preferences,
|
preferences: result.identity.preferences,
|
||||||
instance: result.instance,
|
instance: result.instance,
|
||||||
|
|
|
@ -33,12 +33,13 @@ public struct IdentityDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension IdentityDatabase {
|
public extension IdentityDatabase {
|
||||||
func createIdentity(id: UUID, url: URL, authenticated: Bool) -> AnyPublisher<Never, Error> {
|
func createIdentity(id: UUID, url: URL, authenticated: Bool, pending: Bool) -> AnyPublisher<Never, Error> {
|
||||||
databaseQueue.writePublisher(
|
databaseQueue.writePublisher(
|
||||||
updates: IdentityRecord(
|
updates: IdentityRecord(
|
||||||
id: id,
|
id: id,
|
||||||
url: url,
|
url: url,
|
||||||
authenticated: authenticated,
|
authenticated: authenticated,
|
||||||
|
pending: pending,
|
||||||
lastUsedAt: Date(),
|
lastUsedAt: Date(),
|
||||||
preferences: Identity.Preferences(),
|
preferences: Identity.Preferences(),
|
||||||
instanceURI: nil,
|
instanceURI: nil,
|
||||||
|
@ -99,6 +100,16 @@ public extension IdentityDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func confirmIdentity(id: UUID) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseQueue.writePublisher {
|
||||||
|
try IdentityRecord
|
||||||
|
.filter(Column("id") == id)
|
||||||
|
.updateAll($0, Column("pending").set(to: false))
|
||||||
|
}
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func updatePreferences(_ preferences: Mastodon.Preferences,
|
func updatePreferences(_ preferences: Mastodon.Preferences,
|
||||||
forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
|
forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
|
||||||
databaseQueue.writePublisher {
|
databaseQueue.writePublisher {
|
||||||
|
@ -230,6 +241,7 @@ private extension IdentityDatabase {
|
||||||
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||||
t.column("url", .text).notNull()
|
t.column("url", .text).notNull()
|
||||||
t.column("authenticated", .boolean).notNull()
|
t.column("authenticated", .boolean).notNull()
|
||||||
|
t.column("pending", .boolean).notNull()
|
||||||
t.column("lastUsedAt", .datetime).notNull()
|
t.column("lastUsedAt", .datetime).notNull()
|
||||||
t.column("instanceURI", .text)
|
t.column("instanceURI", .text)
|
||||||
.indexed()
|
.indexed()
|
||||||
|
|
|
@ -8,6 +8,7 @@ struct IdentityRecord: Codable, Hashable, FetchableRecord, PersistableRecord {
|
||||||
let id: UUID
|
let id: UUID
|
||||||
let url: URL
|
let url: URL
|
||||||
let authenticated: Bool
|
let authenticated: Bool
|
||||||
|
let pending: Bool
|
||||||
let lastUsedAt: Date
|
let lastUsedAt: Date
|
||||||
let preferences: Identity.Preferences
|
let preferences: Identity.Preferences
|
||||||
let instanceURI: String?
|
let instanceURI: String?
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
"secondary-navigation.preferences" = "Preferences";
|
"secondary-navigation.preferences" = "Preferences";
|
||||||
"identities.accounts" = "Accounts";
|
"identities.accounts" = "Accounts";
|
||||||
"identities.browsing" = "Browsing";
|
"identities.browsing" = "Browsing";
|
||||||
|
"identities.pending" = "Pending";
|
||||||
"lists.new-list-title" = "New List Title";
|
"lists.new-list-title" = "New List Title";
|
||||||
|
"pending.pending-confirmation" = "Your account is pending confirmation";
|
||||||
"preferences" = "Preferences";
|
"preferences" = "Preferences";
|
||||||
"preferences.posting-reading" = "Posting and Reading";
|
"preferences.posting-reading" = "Posting and Reading";
|
||||||
"preferences.posting" = "Posting";
|
"preferences.posting" = "Posting";
|
||||||
|
|
|
@ -24,6 +24,12 @@ public struct AllIdentitiesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension AllIdentitiesService {
|
public extension AllIdentitiesService {
|
||||||
|
enum IdentityCreation {
|
||||||
|
case authentication
|
||||||
|
case registration(Registration)
|
||||||
|
case browsing
|
||||||
|
}
|
||||||
|
|
||||||
func identityService(id: UUID) throws -> IdentityService {
|
func identityService(id: UUID) throws -> IdentityService {
|
||||||
try IdentityService(id: id, database: database, environment: environment)
|
try IdentityService(id: id, database: database, environment: environment)
|
||||||
}
|
}
|
||||||
|
@ -32,19 +38,50 @@ public extension AllIdentitiesService {
|
||||||
database.immediateMostRecentlyUsedIdentityIDObservation()
|
database.immediateMostRecentlyUsedIdentityIDObservation()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createIdentity(url: URL, authenticated: Bool) -> AnyPublisher<Never, Error> {
|
func createIdentity(url: URL, kind: IdentityCreation) -> AnyPublisher<Never, Error> {
|
||||||
createIdentity(
|
let id = environment.uuid()
|
||||||
url: url,
|
let secrets = Secrets(identityID: id, keychain: environment.keychain)
|
||||||
authenticationPublisher: authenticated
|
|
||||||
? AuthenticationService(url: url, environment: environment).authenticate()
|
|
||||||
: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createIdentity(url: URL, registration: Registration) -> AnyPublisher<Never, Error> {
|
do {
|
||||||
createIdentity(
|
try secrets.setInstanceURL(url)
|
||||||
|
} catch {
|
||||||
|
return Fail(error: error).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
let createIdentityPublisher = database.createIdentity(
|
||||||
|
id: id,
|
||||||
url: url,
|
url: url,
|
||||||
authenticationPublisher: AuthenticationService(url: url, environment: environment)
|
authenticated: kind.authenticated,
|
||||||
.register(registration))
|
pending: kind.pending)
|
||||||
|
.ignoreOutput()
|
||||||
|
.handleEvents(receiveCompletion: {
|
||||||
|
if case .finished = $0 {
|
||||||
|
identitiesCreatedSubject.send(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
let authenticationPublisher: AnyPublisher<(AppAuthorization, AccessToken), Error>
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case .authentication:
|
||||||
|
authenticationPublisher = AuthenticationService(url: url, environment: environment)
|
||||||
|
.authenticate()
|
||||||
|
case let .registration(registration):
|
||||||
|
authenticationPublisher = AuthenticationService(url: url, environment: environment)
|
||||||
|
.register(registration, id: id)
|
||||||
|
case .browsing:
|
||||||
|
return createIdentityPublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticationPublisher
|
||||||
|
.tryMap {
|
||||||
|
try secrets.setClientID($0.clientId)
|
||||||
|
try secrets.setClientSecret($0.clientSecret)
|
||||||
|
try secrets.setAccessToken($1.accessToken)
|
||||||
|
}
|
||||||
|
.flatMap { createIdentityPublisher }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentity(id: UUID) -> AnyPublisher<Never, Error> {
|
func deleteIdentity(id: UUID) -> AnyPublisher<Never, Error> {
|
||||||
|
@ -91,42 +128,22 @@ public extension AllIdentitiesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AllIdentitiesService {
|
private extension AllIdentitiesService.IdentityCreation {
|
||||||
func createIdentity(
|
var authenticated: Bool {
|
||||||
url: URL,
|
switch self {
|
||||||
authenticationPublisher: AnyPublisher<(AppAuthorization, AccessToken), Error>?) -> AnyPublisher<Never, Error> {
|
case .authentication, .registration:
|
||||||
let id = environment.uuid()
|
return true
|
||||||
let secrets = Secrets(identityID: id, keychain: environment.keychain)
|
case .browsing:
|
||||||
|
return false
|
||||||
do {
|
|
||||||
try secrets.setInstanceURL(url)
|
|
||||||
} catch {
|
|
||||||
return Fail(error: error).eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let createIdentityPublisher = database.createIdentity(
|
var pending: Bool {
|
||||||
id: id,
|
switch self {
|
||||||
url: url,
|
case .registration:
|
||||||
authenticated: authenticationPublisher != nil)
|
return true
|
||||||
.ignoreOutput()
|
case .authentication, .browsing:
|
||||||
.handleEvents(receiveCompletion: {
|
return false
|
||||||
if case .finished = $0 {
|
|
||||||
identitiesCreatedSubject.send(id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
|
|
||||||
if let authenticationPublisher = authenticationPublisher {
|
|
||||||
return authenticationPublisher
|
|
||||||
.tryMap {
|
|
||||||
try secrets.setClientID($0.clientId)
|
|
||||||
try secrets.setClientSecret($0.clientSecret)
|
|
||||||
try secrets.setAccessToken($1.accessToken)
|
|
||||||
}
|
|
||||||
.flatMap { createIdentityPublisher }
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
} else {
|
|
||||||
return createIdentityPublisher
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,15 +22,16 @@ struct AuthenticationService {
|
||||||
|
|
||||||
extension AuthenticationService {
|
extension AuthenticationService {
|
||||||
func authenticate() -> AnyPublisher<(AppAuthorization, AccessToken), Error> {
|
func authenticate() -> AnyPublisher<(AppAuthorization, AccessToken), Error> {
|
||||||
let authorization = appAuthorization().share()
|
let authorization = appAuthorization(redirectURI: OAuth.authorizationCallbackURL).share()
|
||||||
|
|
||||||
return authorization
|
return authorization
|
||||||
.zip(authorization.flatMap(authenticate(appAuthorization:)))
|
.zip(authorization.flatMap(authenticate(appAuthorization:)))
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func register(_ registration: Registration) -> AnyPublisher<(AppAuthorization, AccessToken), Error> {
|
func register(_ registration: Registration, id: UUID) -> AnyPublisher<(AppAuthorization, AccessToken), Error> {
|
||||||
let authorization = appAuthorization()
|
let redirectURI = OAuth.registrationCallbackURL.appendingPathComponent(id.uuidString)
|
||||||
|
let authorization = appAuthorization(redirectURI: redirectURI)
|
||||||
.share()
|
.share()
|
||||||
|
|
||||||
return authorization.zip(
|
return authorization.zip(
|
||||||
|
@ -42,7 +43,7 @@ extension AuthenticationService {
|
||||||
grantType: OAuth.registrationGrantType,
|
grantType: OAuth.registrationGrantType,
|
||||||
scopes: OAuth.scopes,
|
scopes: OAuth.scopes,
|
||||||
code: nil,
|
code: nil,
|
||||||
redirectURI: OAuth.callbackURL.absoluteString))
|
redirectURI: redirectURI.absoluteString))
|
||||||
.flatMap { accessToken -> AnyPublisher<AccessToken, Error> in
|
.flatMap { accessToken -> AnyPublisher<AccessToken, Error> in
|
||||||
mastodonAPIClient.accessToken = accessToken.accessToken
|
mastodonAPIClient.accessToken = accessToken.accessToken
|
||||||
|
|
||||||
|
@ -62,7 +63,8 @@ private extension AuthenticationService {
|
||||||
static let authorizationCodeGrantType = "authorization_code"
|
static let authorizationCodeGrantType = "authorization_code"
|
||||||
static let registrationGrantType = "client_credentials"
|
static let registrationGrantType = "client_credentials"
|
||||||
static let callbackURLScheme = "metatext"
|
static let callbackURLScheme = "metatext"
|
||||||
static let callbackURL = URL(string: "\(callbackURLScheme)://oauth.callback")!
|
static let authorizationCallbackURL = URL(string: "\(callbackURLScheme)://oauth.callback")!
|
||||||
|
static let registrationCallbackURL = URL(string: "https://metatext.link/confirmation")!
|
||||||
static let website = URL(string: "https://metabolist.com/metatext")!
|
static let website = URL(string: "https://metabolist.com/metatext")!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,11 +84,11 @@ private extension AuthenticationService {
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
func appAuthorization() -> AnyPublisher<AppAuthorization, Error> {
|
func appAuthorization(redirectURI: URL) -> AnyPublisher<AppAuthorization, Error> {
|
||||||
mastodonAPIClient.request(
|
mastodonAPIClient.request(
|
||||||
AppAuthorizationEndpoint.apps(
|
AppAuthorizationEndpoint.apps(
|
||||||
clientName: OAuth.clientName,
|
clientName: OAuth.clientName,
|
||||||
redirectURI: OAuth.callbackURL.absoluteString,
|
redirectURI: redirectURI.absoluteString,
|
||||||
scopes: OAuth.scopes,
|
scopes: OAuth.scopes,
|
||||||
website: OAuth.website))
|
website: OAuth.website))
|
||||||
}
|
}
|
||||||
|
@ -102,7 +104,7 @@ private extension AuthenticationService {
|
||||||
.init(name: "client_id", value: appAuthorization.clientId),
|
.init(name: "client_id", value: appAuthorization.clientId),
|
||||||
.init(name: "scope", value: OAuth.scopes),
|
.init(name: "scope", value: OAuth.scopes),
|
||||||
.init(name: "response_type", value: "code"),
|
.init(name: "response_type", value: "code"),
|
||||||
.init(name: "redirect_uri", value: OAuth.callbackURL.absoluteString)
|
.init(name: "redirect_uri", value: OAuth.authorizationCallbackURL.absoluteString)
|
||||||
]
|
]
|
||||||
|
|
||||||
guard let authorizationURL = authorizationURLComponents.url else {
|
guard let authorizationURL = authorizationURLComponents.url else {
|
||||||
|
@ -137,7 +139,7 @@ private extension AuthenticationService {
|
||||||
grantType: OAuth.authorizationCodeGrantType,
|
grantType: OAuth.authorizationCodeGrantType,
|
||||||
scopes: OAuth.scopes,
|
scopes: OAuth.scopes,
|
||||||
code: $0,
|
code: $0,
|
||||||
redirectURI: OAuth.callbackURL.absoluteString))
|
redirectURI: OAuth.authorizationCallbackURL.absoluteString))
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,10 @@ public extension IdentityService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func confirmIdentity() -> AnyPublisher<Never, Error> {
|
||||||
|
identityDatabase.confirmIdentity(id: identityID)
|
||||||
|
}
|
||||||
|
|
||||||
func identitiesObservation() -> AnyPublisher<[Identity], Error> {
|
func identitiesObservation() -> AnyPublisher<[Identity], Error> {
|
||||||
identityDatabase.identitiesObservation()
|
identityDatabase.identitiesObservation()
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ let db: IdentityDatabase = {
|
||||||
try! secrets.setInstanceURL(.previewInstanceURL)
|
try! secrets.setInstanceURL(.previewInstanceURL)
|
||||||
try! secrets.setAccessToken(UUID().uuidString)
|
try! secrets.setAccessToken(UUID().uuidString)
|
||||||
|
|
||||||
_ = db.createIdentity(id: id, url: .previewInstanceURL, authenticated: true)
|
_ = db.createIdentity(id: id, url: .previewInstanceURL, authenticated: true, pending: false)
|
||||||
.receive(on: ImmediateScheduler.shared)
|
.receive(on: ImmediateScheduler.shared)
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
|
|
||||||
|
|
|
@ -30,11 +30,11 @@ public final class AddIdentityViewModel: ObservableObject {
|
||||||
|
|
||||||
public extension AddIdentityViewModel {
|
public extension AddIdentityViewModel {
|
||||||
func logInTapped() {
|
func logInTapped() {
|
||||||
addIdentity(authenticated: true)
|
addIdentity(kind: .authentication)
|
||||||
}
|
}
|
||||||
|
|
||||||
func browseTapped() {
|
func browseTapped() {
|
||||||
addIdentity(authenticated: false)
|
addIdentity(kind: .browsing)
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshFilter() {
|
func refreshFilter() {
|
||||||
|
@ -93,10 +93,10 @@ private extension AddIdentityViewModel {
|
||||||
.assign(to: &$isPublicTimelineAvailable)
|
.assign(to: &$isPublicTimelineAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addIdentity(authenticated: Bool) {
|
func addIdentity(kind: AllIdentitiesService.IdentityCreation) {
|
||||||
instanceURLService.url(text: urlFieldText).publisher
|
instanceURLService.url(text: urlFieldText).publisher
|
||||||
.map { ($0, authenticated) }
|
.map { ($0, kind) }
|
||||||
.flatMap(allIdentitiesService.createIdentity(url:authenticated:))
|
.flatMap(allIdentitiesService.createIdentity(url:kind:))
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.handleEvents(receiveSubscription: { [weak self] _ in self?.loading = true })
|
.handleEvents(receiveSubscription: { [weak self] _ in self?.loading = true })
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
|
|
|
@ -8,6 +8,7 @@ public final class IdentitiesViewModel: ObservableObject {
|
||||||
public let currentIdentityID: UUID
|
public let currentIdentityID: UUID
|
||||||
@Published public var authenticated = [Identity]()
|
@Published public var authenticated = [Identity]()
|
||||||
@Published public var unauthenticated = [Identity]()
|
@Published public var unauthenticated = [Identity]()
|
||||||
|
@Published public var pending = [Identity]()
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
|
|
||||||
private let identification: Identification
|
private let identification: Identification
|
||||||
|
@ -21,9 +22,11 @@ public final class IdentitiesViewModel: ObservableObject {
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.share()
|
.share()
|
||||||
|
|
||||||
observation.map { $0.filter { $0.authenticated } }
|
observation.map { $0.filter { $0.authenticated && !$0.pending } }
|
||||||
.assign(to: &$authenticated)
|
.assign(to: &$authenticated)
|
||||||
observation.map { $0.filter { !$0.authenticated } }
|
observation.map { $0.filter { !$0.authenticated && !$0.pending } }
|
||||||
.assign(to: &$unauthenticated)
|
.assign(to: &$unauthenticated)
|
||||||
|
observation.map { $0.filter { $0.pending } }
|
||||||
|
.assign(to: &$pending)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,31 +59,34 @@ public extension NavigationViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshIdentity() {
|
func refreshIdentity() {
|
||||||
if identification.identity.authenticated {
|
if identification.identity.pending {
|
||||||
|
identification.service.verifyCredentials()
|
||||||
|
.collect()
|
||||||
|
.map { _ in () }
|
||||||
|
.flatMap(identification.service.confirmIdentity)
|
||||||
|
.sink { _ in } receiveValue: { _ in }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
} else if identification.identity.authenticated {
|
||||||
identification.service.verifyCredentials()
|
identification.service.verifyCredentials()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
identification.service.refreshLists()
|
identification.service.refreshLists()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.sink { _ in }
|
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
identification.service.refreshFilters()
|
identification.service.refreshFilters()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.sink { _ in }
|
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
if identification.identity.preferences.useServerPostingReadingPreferences {
|
if identification.identity.preferences.useServerPostingReadingPreferences {
|
||||||
identification.service.refreshServerPreferences()
|
identification.service.refreshServerPreferences()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.sink { _ in }
|
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
identification.service.refreshInstance()
|
identification.service.refreshInstance()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.sink { _ in }
|
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ public extension RegistrationViewModel {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
allIdentitiesService.createIdentity(url: url, registration: registration)
|
allIdentitiesService.createIdentity(url: url, kind: .registration(registration))
|
||||||
.handleEvents(receiveSubscription: { [weak self] _ in self?.registering = true })
|
.handleEvents(receiveSubscription: { [weak self] _ in self?.registering = true })
|
||||||
.mapError { error -> Error in
|
.mapError { error -> Error in
|
||||||
if error is URLError {
|
if error is URLError {
|
||||||
|
|
|
@ -21,6 +21,7 @@ struct IdentitiesView: View {
|
||||||
}
|
}
|
||||||
section(title: "identities.accounts", identities: viewModel.authenticated)
|
section(title: "identities.accounts", identities: viewModel.authenticated)
|
||||||
section(title: "identities.browsing", identities: viewModel.unauthenticated)
|
section(title: "identities.browsing", identities: viewModel.unauthenticated)
|
||||||
|
section(title: "identities.pending", identities: viewModel.pending)
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
|
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
|
||||||
|
|
|
@ -11,17 +11,23 @@ struct TabNavigationView: View {
|
||||||
@Environment(\.displayScale) var displayScale: CGFloat
|
@Environment(\.displayScale) var displayScale: CGFloat
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $viewModel.selectedTab) {
|
Group {
|
||||||
ForEach(viewModel.tabs) { tab in
|
if viewModel.identification.identity.pending {
|
||||||
NavigationView {
|
pendingView
|
||||||
view(tab: tab)
|
} else {
|
||||||
|
TabView(selection: $viewModel.selectedTab) {
|
||||||
|
ForEach(viewModel.tabs) { tab in
|
||||||
|
NavigationView {
|
||||||
|
view(tab: tab)
|
||||||
|
}
|
||||||
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
|
.tabItem {
|
||||||
|
Label(tab.title, systemImage: tab.systemImageName)
|
||||||
|
.accessibility(label: Text(tab.title))
|
||||||
|
}
|
||||||
|
.tag(tab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
|
||||||
.tabItem {
|
|
||||||
Label(tab.title, systemImage: tab.systemImageName)
|
|
||||||
.accessibility(label: Text(tab.title))
|
|
||||||
}
|
|
||||||
.tag(tab)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.environmentObject(viewModel.identification)
|
.environmentObject(viewModel.identification)
|
||||||
|
@ -40,6 +46,17 @@ struct TabNavigationView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension TabNavigationView {
|
private extension TabNavigationView {
|
||||||
|
@ViewBuilder
|
||||||
|
var pendingView: some View {
|
||||||
|
NavigationView {
|
||||||
|
Text("pending.pending-confirmation")
|
||||||
|
.navigationBarItems(leading: secondaryNavigationButton)
|
||||||
|
.navigationTitle(viewModel.identification.identity.handle)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func view(tab: NavigationViewModel.Tab) -> some View {
|
func view(tab: NavigationViewModel.Tab) -> some View {
|
||||||
switch tab {
|
switch tab {
|
||||||
|
@ -47,7 +64,8 @@ private extension TabNavigationView {
|
||||||
StatusListView(viewModel: viewModel.viewModel(timeline: viewModel.timeline))
|
StatusListView(viewModel: viewModel.viewModel(timeline: viewModel.timeline))
|
||||||
.id(viewModel.timeline.id)
|
.id(viewModel.timeline.id)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
.navigationBarTitle(viewModel.timeline.title, displayMode: .inline)
|
.navigationTitle(viewModel.timeline.title)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
Loading…
Reference in a new issue