mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 01:31:02 +00:00
Post composition wip
This commit is contained in:
parent
fc1104c951
commit
68dc3ffa3f
26 changed files with 512 additions and 98 deletions
9
DB/Sources/DB/Content/CompositionRecord.swift
Normal file
9
DB/Sources/DB/Content/CompositionRecord.swift
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
struct CompositionRecord: Codable, FetchableRecord, PersistableRecord {
|
||||||
|
let id: Composition.Id
|
||||||
|
let text: String
|
||||||
|
}
|
24
DB/Sources/DB/Entities/Composition.swift
Normal file
24
DB/Sources/DB/Entities/Composition.swift
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
public class Composition {
|
||||||
|
public let id: Id
|
||||||
|
public var text: String
|
||||||
|
|
||||||
|
public init(id: Id, text: String) {
|
||||||
|
self.id = id
|
||||||
|
self.text = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Composition {
|
||||||
|
typealias Id = UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Composition {
|
||||||
|
convenience init(record: CompositionRecord) {
|
||||||
|
self.init(id: record.id, text: record.text)
|
||||||
|
}
|
||||||
|
}
|
|
@ -182,6 +182,17 @@ public extension IdentityDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func authenticatedIdentitiesPublisher() -> AnyPublisher<[Identity], Error> {
|
||||||
|
ValueObservation.tracking(
|
||||||
|
IdentityInfo.request(IdentityRecord.order(IdentityRecord.Columns.lastUsedAt.desc))
|
||||||
|
.filter(IdentityRecord.Columns.authenticated == true && IdentityRecord.Columns.pending == false)
|
||||||
|
.fetchAll)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseWriter)
|
||||||
|
.map { $0.map(Identity.init(info:)) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func immediateMostRecentlyUsedIdentityIdPublisher() -> AnyPublisher<Identity.Id?, Error> {
|
func immediateMostRecentlyUsedIdentityIdPublisher() -> AnyPublisher<Identity.Id?, Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
IdentityRecord.select(IdentityRecord.Columns.id)
|
IdentityRecord.select(IdentityRecord.Columns.id)
|
||||||
|
@ -199,6 +210,17 @@ public extension IdentityDatabase {
|
||||||
.map { $0.map(Identity.init(info:)) }
|
.map { $0.map(Identity.init(info:)) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mostRecentAuthenticatedIdentity() throws -> Identity? {
|
||||||
|
guard let info = try databaseWriter.read(
|
||||||
|
IdentityInfo.request(IdentityRecord.order(IdentityRecord.Columns.lastUsedAt.desc))
|
||||||
|
.filter(IdentityRecord.Columns.authenticated == true
|
||||||
|
&& IdentityRecord.Columns.pending == false)
|
||||||
|
.fetchOne)
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
return Identity(info: info)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension IdentityDatabase {
|
private extension IdentityDatabase {
|
||||||
|
|
19
Data Sources/NewStatusDataSource.swift
Normal file
19
Data Sources/NewStatusDataSource.swift
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class NewStatusDataSource: UICollectionViewDiffableDataSource<Int, Composition.Id> {
|
||||||
|
init(collectionView: UICollectionView, viewModelProvider: @escaping (IndexPath) -> CompositionViewModel) {
|
||||||
|
let registration = UICollectionView.CellRegistration<CompositionListCell, CompositionViewModel> {
|
||||||
|
$0.viewModel = $2
|
||||||
|
}
|
||||||
|
|
||||||
|
super.init(collectionView: collectionView) { collectionView, indexPath, _ in
|
||||||
|
collectionView.dequeueConfiguredReusableCell(
|
||||||
|
using: registration,
|
||||||
|
for: indexPath,
|
||||||
|
item: viewModelProvider(indexPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,7 +49,15 @@
|
||||||
D08E52BD257C635800FA2C5F /* NewStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E5291257C53B600FA2C5F /* NewStatusViewController.swift */; };
|
D08E52BD257C635800FA2C5F /* NewStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E5291257C53B600FA2C5F /* NewStatusViewController.swift */; };
|
||||||
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */; };
|
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */; };
|
||||||
D08E52CC257C80E300FA2C5F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0C7D45724F76169001EBDBB /* Localizable.strings */; };
|
D08E52CC257C80E300FA2C5F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0C7D45724F76169001EBDBB /* Localizable.strings */; };
|
||||||
D08E52D2257C811200FA2C5F /* ShareExtensionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52D1257C811200FA2C5F /* ShareExtensionError.swift */; };
|
D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52D1257C811200FA2C5F /* ShareExtensionError+Extensions.swift */; };
|
||||||
|
D08E52DC257D742B00FA2C5F /* CompositionListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */; };
|
||||||
|
D08E52DD257D742B00FA2C5F /* CompositionListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */; };
|
||||||
|
D08E52E3257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */; };
|
||||||
|
D08E52E4257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */; };
|
||||||
|
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; };
|
||||||
|
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; };
|
||||||
|
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA59472522B8B600804347 /* ViewConstants.swift */; };
|
||||||
|
D08E52FD257D78CB00FA2C5F /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; };
|
||||||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
||||||
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; };
|
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; };
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
||||||
|
@ -97,6 +105,11 @@
|
||||||
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B125251A90F400942152 /* AccountListCell.swift */; };
|
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B125251A90F400942152 /* AccountListCell.swift */; };
|
||||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B12D251A97E400942152 /* TableViewController.swift */; };
|
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B12D251A97E400942152 /* TableViewController.swift */; };
|
||||||
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */; };
|
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */; };
|
||||||
|
D0F2D4D1257EE84400986197 /* NewStatusDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */; };
|
||||||
|
D0F2D4D6257EED6100986197 /* NewStatusDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */; };
|
||||||
|
D0F2D4DB257F018300986197 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C6FAB252024BD003D0300 /* Array+Extensions.swift */; };
|
||||||
|
D0F2D54025818C4B00986197 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = D0F2D53F25818C4B00986197 /* Kingfisher */; };
|
||||||
|
D0F2D5452581ABAB00986197 /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; };
|
||||||
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C8E253686F9003EF1EB /* PlayerView.swift */; };
|
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C8E253686F9003EF1EB /* PlayerView.swift */; };
|
||||||
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */; };
|
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
@ -185,7 +198,10 @@
|
||||||
D08E529B257C58D600FA2C5F /* NewStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusView.swift; sourceTree = "<group>"; };
|
D08E529B257C58D600FA2C5F /* NewStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusView.swift; sourceTree = "<group>"; };
|
||||||
D08E52A5257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionNavigationViewController.swift; sourceTree = "<group>"; };
|
D08E52A5257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionNavigationViewController.swift; sourceTree = "<group>"; };
|
||||||
D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareErrorViewController.swift; sourceTree = "<group>"; };
|
D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareErrorViewController.swift; sourceTree = "<group>"; };
|
||||||
D08E52D1257C811200FA2C5F /* ShareExtensionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionError.swift; sourceTree = "<group>"; };
|
D08E52D1257C811200FA2C5F /* ShareExtensionError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShareExtensionError+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionListCell.swift; sourceTree = "<group>"; };
|
||||||
|
D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
D08E52ED257D757100FA2C5F /* CompositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionView.swift; sourceTree = "<group>"; };
|
||||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
||||||
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
|
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
|
||||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
||||||
|
@ -242,6 +258,7 @@
|
||||||
D0F0B125251A90F400942152 /* AccountListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListCell.swift; sourceTree = "<group>"; };
|
D0F0B125251A90F400942152 /* AccountListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListCell.swift; sourceTree = "<group>"; };
|
||||||
D0F0B12D251A97E400942152 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = "<group>"; };
|
D0F0B12D251A97E400942152 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = "<group>"; };
|
||||||
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionItem+Extensions.swift"; sourceTree = "<group>"; };
|
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionItem+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusDataSource.swift; sourceTree = "<group>"; };
|
||||||
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
|
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
|
||||||
D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCache.swift; sourceTree = "<group>"; };
|
D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCache.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
@ -268,6 +285,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D0F2D54025818C4B00986197 /* Kingfisher in Frameworks */,
|
||||||
D08E52B8257C62D500FA2C5F /* ViewModels in Frameworks */,
|
D08E52B8257C62D500FA2C5F /* ViewModels in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -373,7 +391,7 @@
|
||||||
D08E5273257C36CA00FA2C5F /* Info.plist */,
|
D08E5273257C36CA00FA2C5F /* Info.plist */,
|
||||||
D08E5277257C36CB00FA2C5F /* Share Extension.entitlements */,
|
D08E5277257C36CB00FA2C5F /* Share Extension.entitlements */,
|
||||||
D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */,
|
D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */,
|
||||||
D08E52D1257C811200FA2C5F /* ShareExtensionError.swift */,
|
D08E52D1257C811200FA2C5F /* ShareExtensionError+Extensions.swift */,
|
||||||
D08E52A5257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift */,
|
D08E52A5257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift */,
|
||||||
);
|
);
|
||||||
path = "Share Extension";
|
path = "Share Extension";
|
||||||
|
@ -383,6 +401,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
||||||
|
D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */,
|
||||||
);
|
);
|
||||||
path = "Data Sources";
|
path = "Data Sources";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -405,6 +424,9 @@
|
||||||
D0F0B125251A90F400942152 /* AccountListCell.swift */,
|
D0F0B125251A90F400942152 /* AccountListCell.swift */,
|
||||||
D0F0B10D251A868200942152 /* AccountView.swift */,
|
D0F0B10D251A868200942152 /* AccountView.swift */,
|
||||||
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
||||||
|
D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */,
|
||||||
|
D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */,
|
||||||
|
D08E52ED257D757100FA2C5F /* CompositionView.swift */,
|
||||||
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
|
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
|
||||||
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
|
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
|
||||||
D00702282555E51200F38136 /* ConversationListCell.swift */,
|
D00702282555E51200F38136 /* ConversationListCell.swift */,
|
||||||
|
@ -577,6 +599,7 @@
|
||||||
name = "Share Extension";
|
name = "Share Extension";
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
D08E52B7257C62D500FA2C5F /* ViewModels */,
|
D08E52B7257C62D500FA2C5F /* ViewModels */,
|
||||||
|
D0F2D53F25818C4B00986197 /* Kingfisher */,
|
||||||
);
|
);
|
||||||
productName = "Share Extension";
|
productName = "Share Extension";
|
||||||
productReference = D08E526C257C36CA00FA2C5F /* Share Extension.appex */;
|
productReference = D08E526C257C36CA00FA2C5F /* Share Extension.appex */;
|
||||||
|
@ -715,6 +738,7 @@
|
||||||
files = (
|
files = (
|
||||||
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
||||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
||||||
|
D0F2D4D1257EE84400986197 /* NewStatusDataSource.swift in Sources */,
|
||||||
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
||||||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
||||||
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
||||||
|
@ -722,8 +746,10 @@
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
||||||
D08B8D612540DE3B00B1EBEF /* ZoomDismissalInteractionController.swift in Sources */,
|
D08B8D612540DE3B00B1EBEF /* ZoomDismissalInteractionController.swift in Sources */,
|
||||||
D036AA07254B6118009094DF /* NotificationView.swift in Sources */,
|
D036AA07254B6118009094DF /* NotificationView.swift in Sources */,
|
||||||
|
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||||
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
||||||
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
||||||
|
D08E52E3257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */,
|
||||||
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
||||||
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
|
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
||||||
|
@ -743,6 +769,7 @@
|
||||||
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */,
|
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */,
|
||||||
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
||||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
||||||
|
D08E52DC257D742B00FA2C5F /* CompositionListCell.swift in Sources */,
|
||||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
||||||
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
||||||
D08E5292257C53B600FA2C5F /* NewStatusViewController.swift in Sources */,
|
D08E5292257C53B600FA2C5F /* NewStatusViewController.swift in Sources */,
|
||||||
|
@ -804,8 +831,16 @@
|
||||||
files = (
|
files = (
|
||||||
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
||||||
D08E52BD257C635800FA2C5F /* NewStatusViewController.swift in Sources */,
|
D08E52BD257C635800FA2C5F /* NewStatusViewController.swift in Sources */,
|
||||||
D08E52D2257C811200FA2C5F /* ShareExtensionError.swift in Sources */,
|
D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */,
|
||||||
|
D0F2D4DB257F018300986197 /* Array+Extensions.swift in Sources */,
|
||||||
|
D08E52DD257D742B00FA2C5F /* CompositionListCell.swift in Sources */,
|
||||||
|
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||||
|
D0F2D5452581ABAB00986197 /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
||||||
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
||||||
|
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
||||||
|
D0F2D4D6257EED6100986197 /* NewStatusDataSource.swift in Sources */,
|
||||||
|
D08E52FD257D78CB00FA2C5F /* UIColor+Extensions.swift in Sources */,
|
||||||
|
D08E52E4257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1237,6 +1272,11 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = ViewModels;
|
productName = ViewModels;
|
||||||
};
|
};
|
||||||
|
D0F2D53F25818C4B00986197 /* Kingfisher */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D06B492124D4611300642749 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||||
|
productName = Kingfisher;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = D047FA8024C3E21000AF17C5 /* Project object */;
|
rootObject = D047FA8024C3E21000AF17C5 /* Project object */;
|
||||||
|
|
|
@ -8,13 +8,14 @@ import Mastodon
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
public struct AppEnvironment {
|
public struct AppEnvironment {
|
||||||
|
public let uuid: () -> UUID
|
||||||
|
|
||||||
let session: URLSession
|
let session: URLSession
|
||||||
let webAuthSessionType: WebAuthSession.Type
|
let webAuthSessionType: WebAuthSession.Type
|
||||||
let keychain: Keychain.Type
|
let keychain: Keychain.Type
|
||||||
let userDefaults: UserDefaults
|
let userDefaults: UserDefaults
|
||||||
let userNotificationClient: UserNotificationClient
|
let userNotificationClient: UserNotificationClient
|
||||||
let reduceMotion: () -> Bool
|
let reduceMotion: () -> Bool
|
||||||
let uuid: () -> UUID
|
|
||||||
let inMemoryContent: Bool
|
let inMemoryContent: Bool
|
||||||
let fixtureDatabase: IdentityDatabase?
|
let fixtureDatabase: IdentityDatabase?
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import DB
|
||||||
|
|
||||||
|
public typealias Composition = DB.Composition
|
|
@ -39,6 +39,14 @@ public extension AllIdentitiesService {
|
||||||
database.immediateMostRecentlyUsedIdentityIdPublisher()
|
database.immediateMostRecentlyUsedIdentityIdPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func authenticatedIdentitiesPublisher() -> AnyPublisher<[Identity], Error> {
|
||||||
|
database.authenticatedIdentitiesPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func mostRecentAuthenticatedIdentity() throws -> Identity? {
|
||||||
|
try database.mostRecentAuthenticatedIdentity()
|
||||||
|
}
|
||||||
|
|
||||||
func createIdentity(url: URL, kind: IdentityCreation) -> AnyPublisher<Never, Error> {
|
func createIdentity(url: URL, kind: IdentityCreation) -> AnyPublisher<Never, Error> {
|
||||||
let id = environment.uuid()
|
let id = environment.uuid()
|
||||||
let secrets = Secrets(identityId: id, keychain: environment.keychain)
|
let secrets = Secrets(identityId: id, keychain: environment.keychain)
|
||||||
|
|
|
@ -228,10 +228,6 @@ public extension IdentityService {
|
||||||
func domainBlocksService() -> DomainBlocksService {
|
func domainBlocksService() -> DomainBlocksService {
|
||||||
DomainBlocksService(mastodonAPIClient: mastodonAPIClient)
|
DomainBlocksService(mastodonAPIClient: mastodonAPIClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStatusService() -> NewStatusService {
|
|
||||||
NewStatusService(id: id, identityDatabase: identityDatabase, environment: environment)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension IdentityService {
|
private extension IdentityService {
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import Combine
|
|
||||||
import DB
|
|
||||||
import Foundation
|
|
||||||
import Mastodon
|
|
||||||
import MastodonAPI
|
|
||||||
import Secrets
|
|
||||||
|
|
||||||
public struct NewStatusService {
|
|
||||||
private var id: Identity.Id
|
|
||||||
private let identityDatabase: IdentityDatabase
|
|
||||||
private let environment: AppEnvironment
|
|
||||||
|
|
||||||
public init(id: Identity.Id, identityDatabase: IdentityDatabase, environment: AppEnvironment) {
|
|
||||||
self.id = id
|
|
||||||
self.identityDatabase = identityDatabase
|
|
||||||
self.environment = environment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension NewStatusService {
|
|
||||||
func mastodonAPIClient() throws -> MastodonAPIClient {
|
|
||||||
let secrets = Secrets(
|
|
||||||
identityId: id,
|
|
||||||
keychain: environment.keychain)
|
|
||||||
let mastodonAPIClient = MastodonAPIClient(
|
|
||||||
session: environment.session,
|
|
||||||
instanceURL: try secrets.getInstanceURL())
|
|
||||||
|
|
||||||
mastodonAPIClient.accessToken = try secrets.getAccessToken()
|
|
||||||
|
|
||||||
return mastodonAPIClient
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +1,10 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import ViewModels
|
||||||
enum ShareExtensionError: Error {
|
|
||||||
case noAccountFound
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ShareExtensionError: LocalizedError {
|
extension ShareExtensionError: LocalizedError {
|
||||||
var errorDescription: String? {
|
public var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .noAccountFound:
|
case .noAccountFound:
|
||||||
return NSLocalizedString("share-extension-error.no-account-found", comment: "")
|
return NSLocalizedString("share-extension-error.no-account-found", comment: "")
|
|
@ -7,20 +7,26 @@ import ViewModels
|
||||||
|
|
||||||
@objc(ShareExtensionNavigationViewController)
|
@objc(ShareExtensionNavigationViewController)
|
||||||
class ShareExtensionNavigationViewController: UINavigationController {
|
class ShareExtensionNavigationViewController: UINavigationController {
|
||||||
|
private let viewModel = ShareExtensionNavigationViewModel(
|
||||||
|
environment: .live(
|
||||||
|
userNotificationCenter: .current(),
|
||||||
|
reduceMotion: { UIAccessibility.isReduceMotionEnabled }))
|
||||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
||||||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||||
|
|
||||||
let viewModel: NewStatusViewModel
|
let newStatusViewModel: NewStatusViewModel
|
||||||
|
|
||||||
do {
|
do {
|
||||||
viewModel = try newStatusViewModel()
|
newStatusViewModel = try viewModel.newStatusViewModel()
|
||||||
} catch {
|
} catch {
|
||||||
setViewControllers([ShareErrorViewController(error: error)], animated: false)
|
setViewControllers([ShareErrorViewController(error: error)], animated: false)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setViewControllers([NewStatusViewController(viewModel: viewModel)], animated: false)
|
setViewControllers(
|
||||||
|
[NewStatusViewController(viewModel: newStatusViewModel, isShareExtension: true)],
|
||||||
|
animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
|
@ -28,23 +34,3 @@ class ShareExtensionNavigationViewController: UINavigationController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ShareExtensionNavigationViewController {
|
|
||||||
func newStatusViewModel() throws -> NewStatusViewModel {
|
|
||||||
let environment = AppEnvironment.live(
|
|
||||||
userNotificationCenter: .current(),
|
|
||||||
reduceMotion: { UIAccessibility.isReduceMotionEnabled })
|
|
||||||
let allIdentitiesService = try AllIdentitiesService(environment: environment)
|
|
||||||
|
|
||||||
var recentId: Identity.Id?
|
|
||||||
|
|
||||||
_ = allIdentitiesService.immediateMostRecentlyUsedIdentityIdPublisher()
|
|
||||||
.sink { _ in } receiveValue: { recentId = $0 }
|
|
||||||
|
|
||||||
guard let id = recentId else { throw ShareExtensionError.noAccountFound }
|
|
||||||
|
|
||||||
let newStatusService = try allIdentitiesService.identityService(id: id).newStatusService()
|
|
||||||
|
|
||||||
return NewStatusViewModel(service: newStatusService)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,35 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Kingfisher
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
class NewStatusViewController: UIViewController {
|
class NewStatusViewController: UICollectionViewController {
|
||||||
private let viewModel: NewStatusViewModel
|
private let viewModel: NewStatusViewModel
|
||||||
|
private let isShareExtension: Bool
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(viewModel: NewStatusViewModel) {
|
private lazy var dataSource: NewStatusDataSource = {
|
||||||
|
.init(collectionView: collectionView, viewModelProvider: viewModel.viewModel(indexPath:))
|
||||||
|
}()
|
||||||
|
|
||||||
|
init(viewModel: NewStatusViewModel, isShareExtension: Bool) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
self.isShareExtension = isShareExtension
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
let configuration = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
|
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||||
|
|
||||||
|
super.init(collectionViewLayout: layout)
|
||||||
|
|
||||||
|
viewModel.$identification
|
||||||
|
.sink { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.setupBarButtonItems(identification: $0)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
|
@ -20,18 +40,103 @@ class NewStatusViewController: UIViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
collectionView.dataSource = dataSource
|
||||||
|
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
navigationItem.leftBarButtonItem = .init(
|
setupBarButtonItems(identification: viewModel.identification)
|
||||||
systemItem: .close,
|
|
||||||
primaryAction: UIAction { [weak self] _ in self?.extensionContext?.completeRequest(returningItems: nil) })
|
viewModel.$compositionViewModels.sink { [weak self] in
|
||||||
|
self?.dataSource.apply([$0.map(\.composition.id)].snapshot()) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let collectionView = self?.collectionView,
|
||||||
|
collectionView.indexPathsForSelectedItems?.isEmpty ?? false {
|
||||||
|
collectionView.selectItem(
|
||||||
|
at: collectionView.indexPathsForVisibleItems.first,
|
||||||
|
animated: false,
|
||||||
|
scrollPosition: .top)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didMove(toParent parent: UIViewController?) {
|
override func didMove(toParent parent: UIViewController?) {
|
||||||
super.didMove(toParent: parent)
|
super.didMove(toParent: parent)
|
||||||
|
|
||||||
parent?.navigationItem.leftBarButtonItem = .init(
|
setupBarButtonItems(identification: viewModel.identification)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView,
|
||||||
|
willDisplay cell: UICollectionViewCell,
|
||||||
|
forItemAt indexPath: IndexPath) {
|
||||||
|
((cell as? CompositionListCell)?.contentView as? CompositionView)?.textView.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupBarButtonItems(identification: Identification) {
|
||||||
|
let target = isShareExtension ? self : parent
|
||||||
|
let closeButton = UIBarButtonItem(
|
||||||
systemItem: .close,
|
systemItem: .close,
|
||||||
primaryAction: UIAction { [weak self] _ in self?.presentingViewController?.dismiss(animated: true) })
|
primaryAction: UIAction { [weak self] _ in self?.dismiss() })
|
||||||
|
|
||||||
|
target?.navigationItem.leftBarButtonItem = closeButton
|
||||||
|
target?.navigationItem.titleView = viewModel.canChangeIdentity
|
||||||
|
? changeIdentityButton(identification: identification)
|
||||||
|
: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismiss() {
|
||||||
|
if isShareExtension {
|
||||||
|
extensionContext?.completeRequest(returningItems: nil)
|
||||||
|
} else {
|
||||||
|
presentingViewController?.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewStatusViewController: UITextViewDelegate {
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
collectionView.collectionViewLayout.invalidateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NewStatusViewController {
|
||||||
|
func changeIdentityButton(identification: Identification) -> UIButton {
|
||||||
|
let changeIdentityButton = UIButton()
|
||||||
|
let downsampled = KingfisherOptionsInfo.downsampled(
|
||||||
|
dimension: .barButtonItemDimension,
|
||||||
|
scaleFactor: UIScreen.main.scale)
|
||||||
|
|
||||||
|
let menuItems = viewModel.authenticatedIdentities
|
||||||
|
.filter { $0.id != identification.identity.id }
|
||||||
|
.map { identity in
|
||||||
|
UIDeferredMenuElement { completion in
|
||||||
|
let action = UIAction(title: identity.handle) { [weak self] _ in
|
||||||
|
self?.viewModel.setIdentity(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = identity.image {
|
||||||
|
KingfisherManager.shared.retrieveImage(with: image, options: downsampled) {
|
||||||
|
if case let .success(value) = $0 {
|
||||||
|
action.image = value.image
|
||||||
|
}
|
||||||
|
|
||||||
|
completion([action])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion([action])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeIdentityButton.kf.setImage(
|
||||||
|
with: identification.identity.image,
|
||||||
|
for: .normal,
|
||||||
|
options: downsampled)
|
||||||
|
changeIdentityButton.showsMenuAsPrimaryAction = true
|
||||||
|
changeIdentityButton.menu = UIMenu(children: menuItems)
|
||||||
|
|
||||||
|
return changeIdentityButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,11 +91,7 @@ public extension DomainBlocksViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension NewStatusViewModel {
|
public extension NewStatusViewModel {
|
||||||
static let preview = NewStatusViewModel(
|
static let preview = RootViewModel.preview.newStatusViewModel(identification: .preview)
|
||||||
service: .init(
|
|
||||||
id: identityId,
|
|
||||||
identityDatabase: db,
|
|
||||||
environment: environment))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:enable force_try
|
// swiftlint:enable force_try
|
||||||
|
|
19
ViewModels/Sources/ViewModels/CompositionViewModel.swift
Normal file
19
ViewModels/Sources/ViewModels/CompositionViewModel.swift
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public final class CompositionViewModel: ObservableObject {
|
||||||
|
public let composition: Composition
|
||||||
|
@Published public private(set) var identification: Identification
|
||||||
|
|
||||||
|
init(composition: Composition,
|
||||||
|
identification: Identification,
|
||||||
|
identificationPublisher: AnyPublisher<Identification, Never>) {
|
||||||
|
self.composition = composition
|
||||||
|
self.identification = identification
|
||||||
|
identificationPublisher.assign(to: &$identification)
|
||||||
|
}
|
||||||
|
}
|
5
ViewModels/Sources/ViewModels/Entities/Composition.swift
Normal file
5
ViewModels/Sources/ViewModels/Entities/Composition.swift
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public typealias Composition = ServiceLayer.Composition
|
|
@ -153,10 +153,6 @@ public extension NavigationViewModel {
|
||||||
collectionService: identification.service.service(timeline: .bookmarks),
|
collectionService: identification.service.service(timeline: .bookmarks),
|
||||||
identification: identification)
|
identification: identification)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStatusViewModel() -> NewStatusViewModel {
|
|
||||||
NewStatusViewModel(service: identification.service.newStatusService())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NavigationViewModel.Tab: Identifiable {
|
extension NavigationViewModel.Tab: Identifiable {
|
||||||
|
|
|
@ -6,9 +6,53 @@ import Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public final class NewStatusViewModel: ObservableObject {
|
public final class NewStatusViewModel: ObservableObject {
|
||||||
private let service: NewStatusService
|
@Published public private(set) var compositionViewModels = [CompositionViewModel]()
|
||||||
|
@Published public private(set) var identification: Identification
|
||||||
|
@Published public private(set) var authenticatedIdentities = [Identity]()
|
||||||
|
@Published public var canChangeIdentity = true
|
||||||
|
@Published public var alertItem: AlertItem?
|
||||||
|
|
||||||
public init(service: NewStatusService) {
|
private let allIdentitiesService: AllIdentitiesService
|
||||||
self.service = service
|
private let environment: AppEnvironment
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
public init(allIdentitiesService: AllIdentitiesService,
|
||||||
|
identification: Identification,
|
||||||
|
environment: AppEnvironment) {
|
||||||
|
self.allIdentitiesService = allIdentitiesService
|
||||||
|
self.identification = identification
|
||||||
|
self.environment = environment
|
||||||
|
compositionViewModels = [CompositionViewModel(
|
||||||
|
composition: .init(id: environment.uuid(), text: ""),
|
||||||
|
identification: identification,
|
||||||
|
identificationPublisher: $identification.eraseToAnyPublisher())]
|
||||||
|
allIdentitiesService.authenticatedIdentitiesPublisher()
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.assign(to: &$authenticatedIdentities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension NewStatusViewModel {
|
||||||
|
func viewModel(indexPath: IndexPath) -> CompositionViewModel {
|
||||||
|
compositionViewModels[indexPath.row]
|
||||||
|
}
|
||||||
|
|
||||||
|
func setIdentity(_ identity: Identity) {
|
||||||
|
let identityService: IdentityService
|
||||||
|
|
||||||
|
do {
|
||||||
|
identityService = try allIdentitiesService.identityService(id: identity.id)
|
||||||
|
} catch {
|
||||||
|
alertItem = AlertItem(error: error)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identification = Identification(
|
||||||
|
identity: identity,
|
||||||
|
publisher: identityService.identityPublisher(immediate: false)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self),
|
||||||
|
service: identityService,
|
||||||
|
environment: environment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,13 @@ public extension RootViewModel {
|
||||||
allIdentitiesService: allIdentitiesService,
|
allIdentitiesService: allIdentitiesService,
|
||||||
instanceURLService: InstanceURLService(environment: environment))
|
instanceURLService: InstanceURLService(environment: environment))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newStatusViewModel(identification: Identification) -> NewStatusViewModel {
|
||||||
|
NewStatusViewModel(
|
||||||
|
allIdentitiesService: allIdentitiesService,
|
||||||
|
identification: identification,
|
||||||
|
environment: environment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension RootViewModel {
|
private extension RootViewModel {
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public enum ShareExtensionError: Error {
|
||||||
|
case noAccountFound
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class ShareExtensionNavigationViewModel: ObservableObject {
|
||||||
|
@Published public var alertItem: AlertItem?
|
||||||
|
|
||||||
|
private let environment: AppEnvironment
|
||||||
|
|
||||||
|
public init(environment: AppEnvironment) {
|
||||||
|
self.environment = environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension ShareExtensionNavigationViewModel {
|
||||||
|
func newStatusViewModel() throws -> NewStatusViewModel {
|
||||||
|
let allIdentitiesService = try AllIdentitiesService(environment: environment)
|
||||||
|
|
||||||
|
guard let identity = try allIdentitiesService.mostRecentAuthenticatedIdentity()
|
||||||
|
else { throw ShareExtensionError.noAccountFound }
|
||||||
|
|
||||||
|
let identityService = try allIdentitiesService.identityService(id: identity.id)
|
||||||
|
let identification = Identification(
|
||||||
|
identity: identity,
|
||||||
|
publisher: identityService.identityPublisher(immediate: false)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self),
|
||||||
|
service: identityService,
|
||||||
|
environment: environment)
|
||||||
|
|
||||||
|
return NewStatusViewModel(
|
||||||
|
allIdentitiesService: allIdentitiesService,
|
||||||
|
identification: identification,
|
||||||
|
environment: environment)
|
||||||
|
}
|
||||||
|
}
|
18
Views/CompositionContentConfiguration.swift
Normal file
18
Views/CompositionContentConfiguration.swift
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct CompositionContentConfiguration {
|
||||||
|
let viewModel: CompositionViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CompositionContentConfiguration: UIContentConfiguration {
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
CompositionView(configuration: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> CompositionContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
23
Views/CompositionListCell.swift
Normal file
23
Views/CompositionListCell.swift
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
class CompositionListCell: UICollectionViewListCell {
|
||||||
|
var viewModel: CompositionViewModel?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
contentConfiguration = CompositionContentConfiguration(viewModel: viewModel).updated(for: state)
|
||||||
|
backgroundConfiguration = UIBackgroundConfiguration.clear().updated(for: state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isSelected: Bool {
|
||||||
|
didSet {
|
||||||
|
if isSelected {
|
||||||
|
(contentView as? CompositionView)?.textView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
Views/CompositionView.swift
Normal file
81
Views/CompositionView.swift
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Kingfisher
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class CompositionView: UIView {
|
||||||
|
let avatarImageView = UIImageView()
|
||||||
|
let textView = UITextView()
|
||||||
|
|
||||||
|
private var compositionConfiguration: CompositionContentConfiguration
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(configuration: CompositionContentConfiguration) {
|
||||||
|
self.compositionConfiguration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
applyCompositionConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CompositionView: UIContentView {
|
||||||
|
var configuration: UIContentConfiguration {
|
||||||
|
get { compositionConfiguration }
|
||||||
|
set {
|
||||||
|
guard let compositionConfiguration = newValue as? CompositionContentConfiguration else { return }
|
||||||
|
|
||||||
|
self.compositionConfiguration = compositionConfiguration
|
||||||
|
|
||||||
|
applyCompositionConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CompositionView {
|
||||||
|
func initialSetup() {
|
||||||
|
addSubview(avatarImageView)
|
||||||
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
avatarImageView.layer.cornerRadius = .avatarDimension / 2
|
||||||
|
avatarImageView.clipsToBounds = true
|
||||||
|
|
||||||
|
let stackView = UIStackView()
|
||||||
|
|
||||||
|
addSubview(stackView)
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.axis = .vertical
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(textView)
|
||||||
|
textView.isScrollEnabled = false
|
||||||
|
textView.adjustsFontForContentSizeCategory = true
|
||||||
|
textView.font = .preferredFont(forTextStyle: .body)
|
||||||
|
textView.textContainer.lineFragmentPadding = 0
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension),
|
||||||
|
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),
|
||||||
|
avatarImageView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||||
|
avatarImageView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
|
avatarImageView.bottomAnchor.constraint(lessThanOrEqualTo: readableContentGuide.bottomAnchor),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: .defaultSpacing),
|
||||||
|
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
compositionConfiguration.viewModel.$identification.map(\.identity.image)
|
||||||
|
.sink { [weak self] in self?.avatarImageView.kf.setImage(with: $0) }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyCompositionConfiguration() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ struct NewStatusView: UIViewControllerRepresentable {
|
||||||
let viewModelClosure: () -> NewStatusViewModel
|
let viewModelClosure: () -> NewStatusViewModel
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> NewStatusViewController {
|
func makeUIViewController(context: Context) -> NewStatusViewController {
|
||||||
NewStatusViewController(viewModel: viewModelClosure())
|
NewStatusViewController(viewModel: viewModelClosure(), isShareExtension: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIViewController(_ uiViewController: NewStatusViewController, context: Context) {
|
func updateUIViewController(_ uiViewController: NewStatusViewController, context: Context) {
|
||||||
|
|
|
@ -41,9 +41,11 @@ struct TabNavigationView: View {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
.fullScreenCover(isPresented: $viewModel.presentingNewStatus) {
|
.fullScreenCover(isPresented: $viewModel.presentingNewStatus) {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
NewStatusView(viewModelClosure: viewModel.newStatusViewModel)
|
NewStatusView {
|
||||||
.edgesIgnoringSafeArea(.all)
|
rootViewModel.newStatusViewModel(identification: viewModel.identification)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
}
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
|
@ -137,7 +139,9 @@ private extension TabNavigationView {
|
||||||
viewModel.presentingSecondaryNavigation.toggle()
|
viewModel.presentingSecondaryNavigation.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
KFImage(viewModel.identification.identity.image,
|
KFImage(viewModel.identification.identity.image,
|
||||||
options: .downsampled(dimension: 28, scaleFactor: displayScale))
|
options: .downsampled(
|
||||||
|
dimension: .barButtonItemDimension,
|
||||||
|
scaleFactor: displayScale))
|
||||||
.placeholder { Image(systemName: "gear") }
|
.placeholder { Image(systemName: "gear") }
|
||||||
.renderingMode(.original)
|
.renderingMode(.original)
|
||||||
.contextMenu(ContextMenu {
|
.contextMenu(ContextMenu {
|
||||||
|
@ -149,7 +153,9 @@ private extension TabNavigationView {
|
||||||
title: { Text(recentIdentity.handle) },
|
title: { Text(recentIdentity.handle) },
|
||||||
icon: {
|
icon: {
|
||||||
KFImage(recentIdentity.image,
|
KFImage(recentIdentity.image,
|
||||||
options: .downsampled(dimension: 28, scaleFactor: displayScale))
|
options: .downsampled(
|
||||||
|
dimension: .barButtonItemDimension,
|
||||||
|
scaleFactor: displayScale))
|
||||||
.renderingMode(.original)
|
.renderingMode(.original)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ extension CGFloat {
|
||||||
static let avatarDimension: Self = 50
|
static let avatarDimension: Self = 50
|
||||||
static let hairline = 1 / UIScreen.main.scale
|
static let hairline = 1 / UIScreen.main.scale
|
||||||
static let minimumButtonDimension: Self = 44
|
static let minimumButtonDimension: Self = 44
|
||||||
|
static let barButtonItemDimension: Self = 28
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimeInterval {
|
extension TimeInterval {
|
||||||
|
|
Loading…
Reference in a new issue