mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
Anonymous browsing improvements
This commit is contained in:
parent
8229eecc3a
commit
335a006f45
18 changed files with 192 additions and 107 deletions
|
@ -87,7 +87,9 @@ public extension ContentDatabase {
|
||||||
try Timeline.list(list).save($0)
|
try Timeline.list(list).save($0)
|
||||||
}
|
}
|
||||||
|
|
||||||
try Timeline.filter(!(Timeline.nonLists.map(\.id) + lists.map(\.id)).contains(Column("id"))).deleteAll($0)
|
try Timeline
|
||||||
|
.filter(!(Timeline.authenticatedDefaults.map(\.id) + lists.map(\.id)).contains(Column("id")))
|
||||||
|
.deleteAll($0)
|
||||||
}
|
}
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
@ -155,7 +157,7 @@ public extension ContentDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
||||||
ValueObservation.tracking(Timeline.filter(!Timeline.nonLists.map(\.id).contains(Column("id")))
|
ValueObservation.tracking(Timeline.filter(!Timeline.authenticatedDefaults.map(\.id).contains(Column("id")))
|
||||||
.order(Column("listTitle").collating(.localizedCaseInsensitiveCompare).asc)
|
.order(Column("listTitle").collating(.localizedCaseInsensitiveCompare).asc)
|
||||||
.fetchAll)
|
.fetchAll)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Mastodon
|
||||||
public struct Identity: Codable, Hashable, Identifiable {
|
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 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?
|
||||||
|
|
|
@ -8,6 +8,7 @@ extension Identity {
|
||||||
self.init(
|
self.init(
|
||||||
id: result.identity.id,
|
id: result.identity.id,
|
||||||
url: result.identity.url,
|
url: result.identity.url,
|
||||||
|
authenticated: result.identity.authenticated,
|
||||||
lastUsedAt: result.identity.lastUsedAt,
|
lastUsedAt: result.identity.lastUsedAt,
|
||||||
preferences: result.identity.preferences,
|
preferences: result.identity.preferences,
|
||||||
instance: result.instance,
|
instance: result.instance,
|
||||||
|
|
|
@ -33,11 +33,12 @@ public struct IdentityDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension IdentityDatabase {
|
public extension IdentityDatabase {
|
||||||
func createIdentity(id: UUID, url: URL) -> AnyPublisher<Never, Error> {
|
func createIdentity(id: UUID, url: URL, authenticated: Bool) -> AnyPublisher<Never, Error> {
|
||||||
databaseQueue.writePublisher(
|
databaseQueue.writePublisher(
|
||||||
updates: IdentityRecord(
|
updates: IdentityRecord(
|
||||||
id: id,
|
id: id,
|
||||||
url: url,
|
url: url,
|
||||||
|
authenticated: authenticated,
|
||||||
lastUsedAt: Date(),
|
lastUsedAt: Date(),
|
||||||
preferences: Identity.Preferences(),
|
preferences: Identity.Preferences(),
|
||||||
instanceURI: nil,
|
instanceURI: nil,
|
||||||
|
@ -161,7 +162,7 @@ public extension IdentityDatabase {
|
||||||
func identitiesObservation() -> AnyPublisher<[Identity], Error> {
|
func identitiesObservation() -> AnyPublisher<[Identity], Error> {
|
||||||
ValueObservation.tracking(Self.identitiesRequest().fetchAll)
|
ValueObservation.tracking(Self.identitiesRequest().fetchAll)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseQueue, scheduling: .immediate)
|
.publisher(in: databaseQueue)
|
||||||
.map { $0.map(Identity.init(result:)) }
|
.map { $0.map(Identity.init(result:)) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -173,7 +174,7 @@ public extension IdentityDatabase {
|
||||||
.limit(9)
|
.limit(9)
|
||||||
.fetchAll)
|
.fetchAll)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseQueue, scheduling: .immediate)
|
.publisher(in: databaseQueue)
|
||||||
.map { $0.map(Identity.init(result:)) }
|
.map { $0.map(Identity.init(result:)) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -230,6 +231,7 @@ private extension IdentityDatabase {
|
||||||
try db.create(table: "identityRecord", ifNotExists: true) { t in
|
try db.create(table: "identityRecord", ifNotExists: true) { t in
|
||||||
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("lastUsedAt", .datetime).notNull()
|
t.column("lastUsedAt", .datetime).notNull()
|
||||||
t.column("instanceURI", .text)
|
t.column("instanceURI", .text)
|
||||||
.indexed()
|
.indexed()
|
||||||
|
|
|
@ -7,6 +7,7 @@ import Mastodon
|
||||||
struct IdentityRecord: Codable, Hashable, FetchableRecord, PersistableRecord {
|
struct IdentityRecord: Codable, Hashable, FetchableRecord, PersistableRecord {
|
||||||
let id: UUID
|
let id: UUID
|
||||||
let url: URL
|
let url: URL
|
||||||
|
let authenticated: Bool
|
||||||
let lastUsedAt: Date
|
let lastUsedAt: Date
|
||||||
let preferences: Identity.Preferences
|
let preferences: Identity.Preferences
|
||||||
let instanceURI: String?
|
let instanceURI: String?
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
"secondary-navigation.manage-accounts" = "Manage Accounts";
|
"secondary-navigation.manage-accounts" = "Manage Accounts";
|
||||||
"secondary-navigation.lists" = "Lists";
|
"secondary-navigation.lists" = "Lists";
|
||||||
"secondary-navigation.preferences" = "Preferences";
|
"secondary-navigation.preferences" = "Preferences";
|
||||||
|
"identities.accounts" = "Accounts";
|
||||||
|
"identities.browsing-anonymously" = "Browsing Anonymously";
|
||||||
"lists.new-list-title" = "New List Title";
|
"lists.new-list-title" = "New List Title";
|
||||||
"preferences" = "Preferences";
|
"preferences" = "Preferences";
|
||||||
"preferences.posting-reading" = "Posting and Reading";
|
"preferences.posting-reading" = "Posting and Reading";
|
||||||
|
|
|
@ -11,7 +11,8 @@ public enum Timeline: Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Timeline {
|
public extension Timeline {
|
||||||
static let nonLists: [Timeline] = [.home, .local, .federated]
|
static let unauthenticatedDefaults: [Timeline] = [.local, .federated]
|
||||||
|
static let authenticatedDefaults: [Timeline] = [.home, .local, .federated]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Timeline: Identifiable {
|
extension Timeline: Identifiable {
|
||||||
|
|
|
@ -35,7 +35,7 @@ public extension Secrets {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SecretsError: Error {
|
public enum SecretsError: Error {
|
||||||
case itemAbsent
|
case itemAbsent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,15 +89,19 @@ public extension Secrets {
|
||||||
return "x'\(passphraseData.base16EncodedString(options: [.uppercase]))'"
|
return "x'\(passphraseData.base16EncodedString(options: [.uppercase]))'"
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteAllItems() throws {
|
func deleteAllItems() {
|
||||||
for item in Secrets.Item.allCases {
|
for item in Secrets.Item.allCases {
|
||||||
switch item.kind {
|
do {
|
||||||
case .genericPassword:
|
switch item.kind {
|
||||||
try keychain.deleteGenericPassword(
|
case .genericPassword:
|
||||||
account: scopedKey(item: item),
|
try keychain.deleteGenericPassword(
|
||||||
service: Self.keychainServiceName)
|
account: scopedKey(item: item),
|
||||||
case .key:
|
service: Self.keychainServiceName)
|
||||||
try keychain.deleteKey(applicationTag: scopedKey(item: item))
|
case .key:
|
||||||
|
try keychain.deleteKey(applicationTag: scopedKey(item: item))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,45 +32,61 @@ public extension AllIdentitiesService {
|
||||||
try IdentityService(id: id, database: database, environment: environment)
|
try IdentityService(id: id, database: database, environment: environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> {
|
func createIdentity(id: UUID, url: URL, authenticated: Bool) -> AnyPublisher<Never, Error> {
|
||||||
database.createIdentity(id: id, url: instanceURL)
|
let secrets = Secrets(identityID: id, keychain: environment.keychain)
|
||||||
}
|
|
||||||
|
|
||||||
func authorizeAndCreateIdentity(id: UUID, url: URL) -> AnyPublisher<Never, Error> {
|
do {
|
||||||
AuthenticationService(url: url, environment: environment)
|
try secrets.setInstanceURL(url)
|
||||||
.authenticate()
|
} catch {
|
||||||
.tryMap {
|
return Fail(error: error).eraseToAnyPublisher()
|
||||||
let secrets = Secrets(identityID: id, keychain: environment.keychain)
|
}
|
||||||
|
|
||||||
try secrets.setInstanceURL(url)
|
let createIdentityPublisher = database.createIdentity(
|
||||||
try secrets.setClientID($0.clientId)
|
id: id,
|
||||||
try secrets.setClientSecret($0.clientSecret)
|
url: url,
|
||||||
try secrets.setAccessToken($1.accessToken)
|
authenticated: authenticated)
|
||||||
}
|
|
||||||
.flatMap { database.createIdentity(id: id, url: url) }
|
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
if authenticated {
|
||||||
|
return AuthenticationService(url: url, environment: environment).authenticate()
|
||||||
|
.tryMap {
|
||||||
|
try secrets.setClientID($0.clientId)
|
||||||
|
try secrets.setClientSecret($0.clientSecret)
|
||||||
|
try secrets.setAccessToken($1.accessToken)
|
||||||
|
}
|
||||||
|
.flatMap { createIdentityPublisher }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
} else {
|
||||||
|
return createIdentityPublisher
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentity(_ identity: Identity) -> AnyPublisher<Never, Error> {
|
func deleteIdentity(id: UUID) -> AnyPublisher<Never, Error> {
|
||||||
let secrets = Secrets(identityID: identity.id, keychain: environment.keychain)
|
database.deleteIdentity(id: id)
|
||||||
let mastodonAPIClient = MastodonAPIClient(session: environment.session, instanceURL: identity.url)
|
.collect()
|
||||||
|
.tryMap { _ -> AnyPublisher<Never, Error> in
|
||||||
|
try ContentDatabase.delete(forIdentityID: id)
|
||||||
|
|
||||||
return database.deleteIdentity(id: identity.id)
|
let secrets = Secrets(identityID: id, keychain: environment.keychain)
|
||||||
.collect()
|
|
||||||
.tryMap { _ in
|
defer { secrets.deleteAllItems() }
|
||||||
DeletionEndpoint.oauthRevoke(
|
|
||||||
token: try secrets.getAccessToken(),
|
do {
|
||||||
clientID: try secrets.getClientID(),
|
return MastodonAPIClient(
|
||||||
clientSecret: try secrets.getClientSecret())
|
session: environment.session,
|
||||||
|
instanceURL: try secrets.getInstanceURL())
|
||||||
|
.request(DeletionEndpoint.oauthRevoke(
|
||||||
|
token: try secrets.getAccessToken(),
|
||||||
|
clientID: try secrets.getClientID(),
|
||||||
|
clientSecret: try secrets.getClientSecret()))
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
} catch {
|
||||||
|
return Empty().eraseToAnyPublisher()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.flatMap(mastodonAPIClient.request)
|
.flatMap { $0 }
|
||||||
.collect()
|
|
||||||
.tryMap { _ in
|
|
||||||
try secrets.deleteAllItems()
|
|
||||||
try ContentDatabase.delete(forIdentityID: identity.id)
|
|
||||||
}
|
|
||||||
.ignoreOutput()
|
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ let db: IdentityDatabase = {
|
||||||
try! secrets.setInstanceURL(url)
|
try! secrets.setInstanceURL(url)
|
||||||
try! secrets.setAccessToken(UUID().uuidString)
|
try! secrets.setAccessToken(UUID().uuidString)
|
||||||
|
|
||||||
_ = db.createIdentity(id: id, url: url)
|
_ = db.createIdentity(id: id, url: url, authenticated: true)
|
||||||
.receive(on: ImmediateScheduler.shared)
|
.receive(on: ImmediateScheduler.shared)
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ public extension AddIdentityViewModel {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
allIdentitiesService.authorizeAndCreateIdentity(id: identityID, url: instanceURL)
|
allIdentitiesService.createIdentity(id: identityID, url: instanceURL, authenticated: true)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.catch { [weak self] error -> Empty<Never, Never> in
|
.catch { [weak self] error -> Empty<Never, Never> in
|
||||||
if case AuthenticationError.canceled = error {
|
if case AuthenticationError.canceled = error {
|
||||||
|
@ -70,7 +70,7 @@ public extension AddIdentityViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Ensure instance has not disabled public preview
|
// TODO: Ensure instance has not disabled public preview
|
||||||
allIdentitiesService.createIdentity(id: identityID, instanceURL: instanceURL)
|
allIdentitiesService.createIdentity(id: identityID, url: instanceURL, authenticated: false)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
guard let self = self, case .finished = $0 else { return }
|
guard let self = self, case .finished = $0 else { return }
|
||||||
|
|
|
@ -6,7 +6,8 @@ import ServiceLayer
|
||||||
|
|
||||||
public final class IdentitiesViewModel: ObservableObject {
|
public final class IdentitiesViewModel: ObservableObject {
|
||||||
public let currentIdentityID: UUID
|
public let currentIdentityID: UUID
|
||||||
@Published public var identities = [Identity]()
|
@Published public var authenticated = [Identity]()
|
||||||
|
@Published public var unauthenticated = [Identity]()
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
|
|
||||||
private let identification: Identification
|
private let identification: Identification
|
||||||
|
@ -16,8 +17,13 @@ public final class IdentitiesViewModel: ObservableObject {
|
||||||
self.identification = identification
|
self.identification = identification
|
||||||
currentIdentityID = identification.identity.id
|
currentIdentityID = identification.identity.id
|
||||||
|
|
||||||
identification.service.identitiesObservation()
|
let observation = identification.service.identitiesObservation()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$identities)
|
.share()
|
||||||
|
|
||||||
|
observation.map { $0.filter { $0.authenticated } }
|
||||||
|
.assign(to: &$authenticated)
|
||||||
|
observation.map { $0.filter { !$0.authenticated } }
|
||||||
|
.assign(to: &$unauthenticated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,8 +70,8 @@ public extension RootViewModel {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentity(_ identity: Identity) {
|
func deleteIdentity(id: UUID) {
|
||||||
allIdentitiesService.deleteIdentity(identity)
|
allIdentitiesService.deleteIdentity(id: id)
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ import ServiceLayer
|
||||||
public final class TabNavigationViewModel: ObservableObject {
|
public final class TabNavigationViewModel: ObservableObject {
|
||||||
@Published public private(set) var identity: Identity
|
@Published public private(set) var identity: Identity
|
||||||
@Published public private(set) var recentIdentities = [Identity]()
|
@Published public private(set) var recentIdentities = [Identity]()
|
||||||
@Published public var timeline = Timeline.home
|
@Published public var timeline: Timeline
|
||||||
@Published public private(set) var timelinesAndLists = Timeline.nonLists
|
@Published public private(set) var timelinesAndLists: [Timeline]
|
||||||
@Published public var presentingSecondaryNavigation = false
|
@Published public var presentingSecondaryNavigation = false
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
public var selectedTab: Tab? = .timelines
|
public var selectedTab: Tab? = .timelines
|
||||||
|
@ -20,20 +20,34 @@ public final class TabNavigationViewModel: ObservableObject {
|
||||||
public init(identification: Identification) {
|
public init(identification: Identification) {
|
||||||
self.identification = identification
|
self.identification = identification
|
||||||
identity = identification.identity
|
identity = identification.identity
|
||||||
identification.$identity.dropFirst().assign(to: &$identity)
|
timeline = identification.service.isAuthorized ? .home : .local
|
||||||
|
timelinesAndLists = identification.service.isAuthorized
|
||||||
|
? Timeline.authenticatedDefaults
|
||||||
|
: Timeline.unauthenticatedDefaults
|
||||||
|
|
||||||
|
identification.$identity.dropFirst().assign(to: &$identity)
|
||||||
identification.service.recentIdentitiesObservation()
|
identification.service.recentIdentitiesObservation()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$recentIdentities)
|
.assign(to: &$recentIdentities)
|
||||||
|
|
||||||
identification.service.listsObservation()
|
if identification.service.isAuthorized {
|
||||||
.map { Timeline.nonLists + $0 }
|
identification.service.listsObservation()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.map { Timeline.authenticatedDefaults + $0 }
|
||||||
.assign(to: &$timelinesAndLists)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.assign(to: &$timelinesAndLists)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension TabNavigationViewModel {
|
public extension TabNavigationViewModel {
|
||||||
|
var tabs: [Tab] {
|
||||||
|
if identification.service.isAuthorized {
|
||||||
|
return Tab.allCases
|
||||||
|
} else {
|
||||||
|
return [.timelines, .explore]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var timelineSubtitle: String {
|
var timelineSubtitle: String {
|
||||||
switch timeline {
|
switch timeline {
|
||||||
case .home, .list:
|
case .home, .list:
|
||||||
|
@ -43,28 +57,16 @@ public extension TabNavigationViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func systemImageName(timeline: Timeline) -> String {
|
|
||||||
switch timeline {
|
|
||||||
case .home: return "house"
|
|
||||||
case .local: return "person.3"
|
|
||||||
case .federated: return "globe"
|
|
||||||
case .list: return "scroll"
|
|
||||||
case .tag: return "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshIdentity() {
|
func refreshIdentity() {
|
||||||
if identification.service.isAuthorized {
|
if identification.service.isAuthorized {
|
||||||
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)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
identification.service.refreshFilters()
|
identification.service.refreshFilters()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
|
@ -92,7 +94,7 @@ public extension TabNavigationViewModel {
|
||||||
public extension TabNavigationViewModel {
|
public extension TabNavigationViewModel {
|
||||||
enum Tab: CaseIterable {
|
enum Tab: CaseIterable {
|
||||||
case timelines
|
case timelines
|
||||||
case search
|
case explore
|
||||||
case notifications
|
case notifications
|
||||||
case messages
|
case messages
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,11 @@ struct AddIdentityView: View {
|
||||||
} else {
|
} else {
|
||||||
Button("add-identity.log-in",
|
Button("add-identity.log-in",
|
||||||
action: viewModel.logInTapped)
|
action: viewModel.logInTapped)
|
||||||
|
Button("add-identity.browse-anonymously", action: viewModel.browseAnonymouslyTapped)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
Button("add-identity.browse-anonymously", action: viewModel.browseAnonymouslyTapped)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
|
||||||
}
|
}
|
||||||
.alertItem($viewModel.alertItem)
|
.alertItem($viewModel.alertItem)
|
||||||
.onReceive(viewModel.addedIdentityID) { id in
|
.onReceive(viewModel.addedIdentityID) { id in
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import KingfisherSwiftUI
|
import KingfisherSwiftUI
|
||||||
|
import struct ServiceLayer.Identity
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -18,9 +19,26 @@ struct IdentitiesView: View {
|
||||||
Label("add", systemImage: "plus.circle")
|
Label("add", systemImage: "plus.circle")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Section {
|
section(title: "identities.accounts", identities: viewModel.authenticated)
|
||||||
|
section(title: "identities.browsing-anonymously", identities: viewModel.unauthenticated)
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
|
||||||
|
EditButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension IdentitiesView {
|
||||||
|
@ViewBuilder
|
||||||
|
func section(title: LocalizedStringKey, identities: [Identity]) -> some View {
|
||||||
|
if identities.isEmpty {
|
||||||
|
EmptyView()
|
||||||
|
} else {
|
||||||
|
Section(header: Text(title)) {
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.identities) { identity in
|
ForEach(identities) { identity in
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
rootViewModel.newIdentitySelected(id: identity.id)
|
rootViewModel.newIdentitySelected(id: identity.id)
|
||||||
|
@ -31,15 +49,26 @@ struct IdentitiesView: View {
|
||||||
options: .downsampled(dimension: 40, scaleFactor: displayScale))
|
options: .downsampled(dimension: 40, scaleFactor: displayScale))
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Spacer()
|
Spacer()
|
||||||
if let account = identity.account {
|
if identity.authenticated {
|
||||||
CustomEmojiText(
|
if let account = identity.account {
|
||||||
text: account.displayName,
|
CustomEmojiText(
|
||||||
emoji: account.emojis,
|
text: account.displayName,
|
||||||
textStyle: .headline)
|
emoji: account.emojis,
|
||||||
|
textStyle: .headline)
|
||||||
|
}
|
||||||
|
Text(identity.handle)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else {
|
||||||
|
Text(identity.handle)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
if let instance = identity.instance {
|
||||||
|
Text(instance.uri)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Text(identity.handle)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -54,16 +83,11 @@ struct IdentitiesView: View {
|
||||||
.onDelete {
|
.onDelete {
|
||||||
guard let index = $0.first else { return }
|
guard let index = $0.first else { return }
|
||||||
|
|
||||||
rootViewModel.deleteIdentity(viewModel.identities[index])
|
rootViewModel.deleteIdentity(id: identities[index].id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
|
|
||||||
EditButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,17 +21,30 @@ struct SecondaryNavigationView: View {
|
||||||
KFImage(tabNavigationViewModel.identity.image,
|
KFImage(tabNavigationViewModel.identity.image,
|
||||||
options: .downsampled(dimension: 50, scaleFactor: displayScale))
|
options: .downsampled(dimension: 50, scaleFactor: displayScale))
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if let account = tabNavigationViewModel.identity.account {
|
if tabNavigationViewModel.identity.authenticated {
|
||||||
CustomEmojiText(
|
if let account = tabNavigationViewModel.identity.account {
|
||||||
text: account.displayName,
|
CustomEmojiText(
|
||||||
emoji: account.emojis,
|
text: account.displayName,
|
||||||
textStyle: .headline)
|
emoji: account.emojis,
|
||||||
|
textStyle: .headline)
|
||||||
|
}
|
||||||
|
Text(tabNavigationViewModel.identity.handle)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
|
} else {
|
||||||
|
Text(tabNavigationViewModel.identity.handle)
|
||||||
|
.font(.headline)
|
||||||
|
if let instance = tabNavigationViewModel.identity.instance {
|
||||||
|
Text(instance.uri)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Text(tabNavigationViewModel.identity.handle)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
.minimumScaleFactor(0.5)
|
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("secondary-navigation.manage-accounts")
|
Text("secondary-navigation.manage-accounts")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
|
|
@ -12,7 +12,7 @@ struct TabNavigationView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $viewModel.selectedTab) {
|
TabView(selection: $viewModel.selectedTab) {
|
||||||
ForEach(TabNavigationViewModel.Tab.allCases) { tab in
|
ForEach(viewModel.tabs) { tab in
|
||||||
NavigationView {
|
NavigationView {
|
||||||
view(tab: tab)
|
view(tab: tab)
|
||||||
}
|
}
|
||||||
|
@ -65,11 +65,11 @@ private extension TabNavigationView {
|
||||||
viewModel.timeline = timeline
|
viewModel.timeline = timeline
|
||||||
} label: {
|
} label: {
|
||||||
Label(timeline.title,
|
Label(timeline.title,
|
||||||
systemImage: viewModel.systemImageName(timeline: timeline))
|
systemImage: timeline.systemImageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: viewModel.systemImageName(timeline: viewModel.timeline))
|
Image(systemName: viewModel.timeline.systemImageName)
|
||||||
})
|
})
|
||||||
default: Text(tab.title)
|
default: Text(tab.title)
|
||||||
}
|
}
|
||||||
|
@ -118,13 +118,23 @@ private extension Timeline {
|
||||||
return "#" + tag
|
return "#" + tag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var systemImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .home: return "house"
|
||||||
|
case .local: return "person.3"
|
||||||
|
case .federated: return "globe"
|
||||||
|
case .list: return "scroll"
|
||||||
|
case .tag: return "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TabNavigationViewModel.Tab {
|
extension TabNavigationViewModel.Tab {
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .timelines: return "Timelines"
|
case .timelines: return "Timelines"
|
||||||
case .search: return "Search"
|
case .explore: return "Explore"
|
||||||
case .notifications: return "Notifications"
|
case .notifications: return "Notifications"
|
||||||
case .messages: return "Messages"
|
case .messages: return "Messages"
|
||||||
}
|
}
|
||||||
|
@ -133,7 +143,7 @@ extension TabNavigationViewModel.Tab {
|
||||||
var systemImageName: String {
|
var systemImageName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .timelines: return "newspaper"
|
case .timelines: return "newspaper"
|
||||||
case .search: return "magnifyingglass"
|
case .explore: return "magnifyingglass"
|
||||||
case .notifications: return "bell"
|
case .notifications: return "bell"
|
||||||
case .messages: return "envelope"
|
case .messages: return "envelope"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue