diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 49f1baf..4b4c4ae 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -73,6 +73,8 @@ D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; }; D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; }; D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; }; + D0A1CA7424DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */; }; + D0A1CA7524DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */; }; D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */; }; D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */; }; D0BEC93824C9632800E864C4 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* RootViewModel.swift */; }; @@ -187,6 +189,7 @@ D074577624D29006004758DB /* StubbingWebAuthSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubbingWebAuthSession.swift; sourceTree = ""; }; D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = ""; }; D081A40424D0F1A8001B016E /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; + D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = ""; }; D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = ""; }; D0BEC93724C9632800E864C4 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = ""; }; D0BEC93A24C96FD500E864C4 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; @@ -397,6 +400,7 @@ D0DB6F1624C665B400D965FE /* Extensions */ = { isa = PBXGroup; children = ( + D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */, D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */, D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */, D081A40424D0F1A8001B016E /* String+Extensions.swift */, @@ -695,6 +699,7 @@ D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */, D0666A7224C6E0D300F3F04B /* Secrets.swift in Sources */, D0BEC95124CA2B7E00E864C4 /* TabNavigation.swift in Sources */, + D0A1CA7424DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */, D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */, D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */, D04FD73C24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */, @@ -760,6 +765,7 @@ D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */, D0BEC94F24CA2B5300E864C4 /* SidebarNavigation.swift in Sources */, D0666A7324C6E0D300F3F04B /* Secrets.swift in Sources */, + D0A1CA7524DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */, D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */, D0C963FF24CC3812003BD330 /* Publisher+Extensions.swift in Sources */, D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */, diff --git a/Shared/Extensions/KingfisherOptionsInfo+Extensions.swift b/Shared/Extensions/KingfisherOptionsInfo+Extensions.swift new file mode 100644 index 0000000..204fb59 --- /dev/null +++ b/Shared/Extensions/KingfisherOptionsInfo+Extensions.swift @@ -0,0 +1,30 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import SwiftUI +import KingfisherSwiftUI +import struct Kingfisher.KingfisherOptionsInfo +import protocol Kingfisher.ImageProcessor +import struct Kingfisher.DownsamplingImageProcessor +import struct Kingfisher.RoundCornerImageProcessor +import struct Kingfisher.FormatIndicatedCacheSerializer + +extension KingfisherOptionsInfo { + static func downsampled(size: CGSize, scaleFactor: CGFloat, rounded: Bool = true) -> Self { + var processor: ImageProcessor = DownsamplingImageProcessor(size: size) + + if rounded { + processor = processor.append(another: RoundCornerImageProcessor(radius: .widthFraction(0.5))) + } + + return [ + .processor(processor), + .scaleFactor(scaleFactor), + .cacheOriginalImage, + .cacheSerializer(FormatIndicatedCacheSerializer.png) + ] + } + + static func downsampled(dimension: CGFloat, scaleFactor: CGFloat, rounded: Bool = true) -> Self { + downsampled(size: CGSize(width: dimension, height: dimension), scaleFactor: scaleFactor, rounded: rounded) + } +} diff --git a/Shared/Model/IdentityDatabase.swift b/Shared/Model/IdentityDatabase.swift index 1e3e135..d792501 100644 --- a/Shared/Model/IdentityDatabase.swift +++ b/Shared/Model/IdentityDatabase.swift @@ -102,17 +102,27 @@ extension IdentityDatabase { StoredIdentity .including(optional: StoredIdentity.instance) .including(optional: StoredIdentity.account) - .asRequest(of: IdentityResult.self).fetchAll) + .asRequest(of: IdentityResult.self) + .fetchAll) .removeDuplicates() .publisher(in: databaseQueue, scheduling: .immediate) .map { $0.map(Identity.init(result:)) } .eraseToAnyPublisher() } - func identityCountObservation() -> AnyPublisher { - ValueObservation.tracking(StoredIdentity.fetchCount) + func recentIdentitiesObservation(excluding: String) -> AnyPublisher<[Identity], Error> { + ValueObservation.tracking( + StoredIdentity + .filter(Column("id") != excluding) + .order(Column("lastUsedAt").desc) + .limit(10) + .including(optional: StoredIdentity.instance) + .including(optional: StoredIdentity.account) + .asRequest(of: IdentityResult.self) + .fetchAll) .removeDuplicates() .publisher(in: databaseQueue, scheduling: .immediate) + .map { $0.map(Identity.init(result:)) } .eraseToAnyPublisher() } diff --git a/Shared/View Models/MainNavigationViewModel.swift b/Shared/View Models/MainNavigationViewModel.swift index bc6b86f..70b7ef9 100644 --- a/Shared/View Models/MainNavigationViewModel.swift +++ b/Shared/View Models/MainNavigationViewModel.swift @@ -5,6 +5,7 @@ import Combine class MainNavigationViewModel: ObservableObject { @Published private(set) var identity: Identity + @Published private(set) var recentIdentities = [Identity]() @Published var presentingSettings = false @Published var alertItem: AlertItem? var selectedTab: Tab? = .timelines @@ -37,6 +38,9 @@ class MainNavigationViewModel: ObservableObject { } observation.assignErrorsToAlertItem(to: \.alertItem, on: self).assign(to: &$identity) + environment.identityDatabase.recentIdentitiesObservation(excluding: identityID) + .assignErrorsToAlertItem(to: \.alertItem, on: self) + .assign(to: &$recentIdentities) environment.identityDatabase.updateLastUsedAt(identityID: identityID) .assignErrorsToAlertItem(to: \.alertItem, on: self) diff --git a/Shared/Views/IdentitiesView.swift b/Shared/Views/IdentitiesView.swift index 77c1af7..24771fd 100644 --- a/Shared/Views/IdentitiesView.swift +++ b/Shared/Views/IdentitiesView.swift @@ -29,5 +29,6 @@ struct IdentitiesView: View { struct IdentitiesView_Previews: PreviewProvider { static var previews: some View { IdentitiesView(viewModel: .development) + .environmentObject(RootViewModel.development) } } diff --git a/Shared/Views/SettingsView.swift b/Shared/Views/SettingsView.swift index 4df7609..3ba30d3 100644 --- a/Shared/Views/SettingsView.swift +++ b/Shared/Views/SettingsView.swift @@ -2,7 +2,6 @@ import SwiftUI import KingfisherSwiftUI -import struct Kingfisher.DownsamplingImageProcessor struct SettingsView: View { @StateObject var viewModel: SettingsViewModel @@ -16,14 +15,7 @@ struct SettingsView: View { Form { HStack { KFImage(viewModel.identity.image, - options: [ - .processor( - DownsamplingImageProcessor(size: CGSize(width: 50, height: 50)) - ), - .scaleFactor(displayScale), - .cacheOriginalImage - ]) - .clipShape(Circle()) + options: .downsampled(dimension: 50, scaleFactor: displayScale)) Text(viewModel.identity.handle) .font(.subheadline) } @@ -89,7 +81,6 @@ private extension View { struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView(viewModel: .development) - .environmentObject(MainNavigationViewModel.development) .environmentObject(RootViewModel.development) } } diff --git a/iOS/TabNavigation.swift b/iOS/TabNavigation.swift index 1db5574..8dedf70 100644 --- a/iOS/TabNavigation.swift +++ b/iOS/TabNavigation.swift @@ -2,7 +2,6 @@ import SwiftUI import KingfisherSwiftUI -import struct Kingfisher.DownsamplingImageProcessor struct TabNavigation: View { @ObservedObject var viewModel: MainNavigationViewModel @@ -37,30 +36,37 @@ struct TabNavigation: View { } private extension TabNavigation { + @ViewBuilder func view(tab: MainNavigationViewModel.Tab) -> some View { - Group { - switch tab { - case .timelines: - TimelineView() - .navigationBarTitle(viewModel.identity.handle, displayMode: .inline) - .navigationBarItems( - leading: Button { - viewModel.presentingSettings.toggle() - } label: { - KFImage(viewModel.identity.image, - options: [ - .processor( - DownsamplingImageProcessor(size: CGSize(width: 28, height: 28)) - ), - .scaleFactor(displayScale), - .cacheOriginalImage - ]) - .placeholder { Image(systemName: "gear") } - .renderingMode(.original) - .clipShape(Circle()) - }) - default: Text(tab.title) - } + switch tab { + case .timelines: + TimelineView() + .navigationBarTitle(viewModel.identity.handle, displayMode: .inline) + .navigationBarItems( + leading: Button { + viewModel.presentingSettings.toggle() + } label: { + KFImage(viewModel.identity.image, + options: .downsampled(dimension: 28, scaleFactor: displayScale)) + .placeholder { Image(systemName: "gear") } + .renderingMode(.original) + .contextMenu(ContextMenu { + ForEach(viewModel.recentIdentities) { recentIdentity in + Button { + rootViewModel.newIdentitySelected(id: recentIdentity.id) + } label: { + Label( + title: { Text(recentIdentity.handle) }, + icon: { + KFImage(recentIdentity.image, + options: .downsampled(dimension: 28, scaleFactor: displayScale)) + .renderingMode(.original) + }) + } + } + }) + }) + default: Text(tab.title) } } } @@ -69,6 +75,7 @@ private extension TabNavigation { struct TabNavigation_Previews: PreviewProvider { static var previews: some View { TabNavigation(viewModel: .development) + .environmentObject(RootViewModel.development) } } #endif