mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-21 15:50:59 +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()
|
||||
}
|
||||
|
||||
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> {
|
||||
ValueObservation.tracking(
|
||||
IdentityRecord.select(IdentityRecord.Columns.id)
|
||||
|
@ -199,6 +210,17 @@ public extension IdentityDatabase {
|
|||
.map { $0.map(Identity.init(info:)) }
|
||||
.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 {
|
||||
|
|
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 */; };
|
||||
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */; };
|
||||
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 */; };
|
||||
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.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 */; };
|
||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B12D251A97E400942152 /* TableViewController.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 */; };
|
||||
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
@ -185,7 +198,10 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; 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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCache.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
@ -268,6 +285,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D0F2D54025818C4B00986197 /* Kingfisher in Frameworks */,
|
||||
D08E52B8257C62D500FA2C5F /* ViewModels in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -373,7 +391,7 @@
|
|||
D08E5273257C36CA00FA2C5F /* Info.plist */,
|
||||
D08E5277257C36CB00FA2C5F /* Share Extension.entitlements */,
|
||||
D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */,
|
||||
D08E52D1257C811200FA2C5F /* ShareExtensionError.swift */,
|
||||
D08E52D1257C811200FA2C5F /* ShareExtensionError+Extensions.swift */,
|
||||
D08E52A5257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift */,
|
||||
);
|
||||
path = "Share Extension";
|
||||
|
@ -383,6 +401,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
||||
D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */,
|
||||
);
|
||||
path = "Data Sources";
|
||||
sourceTree = "<group>";
|
||||
|
@ -405,6 +424,9 @@
|
|||
D0F0B125251A90F400942152 /* AccountListCell.swift */,
|
||||
D0F0B10D251A868200942152 /* AccountView.swift */,
|
||||
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
||||
D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */,
|
||||
D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */,
|
||||
D08E52ED257D757100FA2C5F /* CompositionView.swift */,
|
||||
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
|
||||
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
|
||||
D00702282555E51200F38136 /* ConversationListCell.swift */,
|
||||
|
@ -577,6 +599,7 @@
|
|||
name = "Share Extension";
|
||||
packageProductDependencies = (
|
||||
D08E52B7257C62D500FA2C5F /* ViewModels */,
|
||||
D0F2D53F25818C4B00986197 /* Kingfisher */,
|
||||
);
|
||||
productName = "Share Extension";
|
||||
productReference = D08E526C257C36CA00FA2C5F /* Share Extension.appex */;
|
||||
|
@ -715,6 +738,7 @@
|
|||
files = (
|
||||
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
||||
D0F2D4D1257EE84400986197 /* NewStatusDataSource.swift in Sources */,
|
||||
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
||||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
||||
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
||||
|
@ -722,8 +746,10 @@
|
|||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
||||
D08B8D612540DE3B00B1EBEF /* ZoomDismissalInteractionController.swift in Sources */,
|
||||
D036AA07254B6118009094DF /* NotificationView.swift in Sources */,
|
||||
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
||||
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
||||
D08E52E3257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */,
|
||||
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
||||
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
|
||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
||||
|
@ -743,6 +769,7 @@
|
|||
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */,
|
||||
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
||||
D08E52DC257D742B00FA2C5F /* CompositionListCell.swift in Sources */,
|
||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
||||
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
||||
D08E5292257C53B600FA2C5F /* NewStatusViewController.swift in Sources */,
|
||||
|
@ -804,8 +831,16 @@
|
|||
files = (
|
||||
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.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 */,
|
||||
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
||||
D0F2D4D6257EED6100986197 /* NewStatusDataSource.swift in Sources */,
|
||||
D08E52FD257D78CB00FA2C5F /* UIColor+Extensions.swift in Sources */,
|
||||
D08E52E4257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1237,6 +1272,11 @@
|
|||
isa = XCSwiftPackageProductDependency;
|
||||
productName = ViewModels;
|
||||
};
|
||||
D0F2D53F25818C4B00986197 /* Kingfisher */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D06B492124D4611300642749 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = D047FA8024C3E21000AF17C5 /* Project object */;
|
||||
|
|
|
@ -8,13 +8,14 @@ import Mastodon
|
|||
import UserNotifications
|
||||
|
||||
public struct AppEnvironment {
|
||||
public let uuid: () -> UUID
|
||||
|
||||
let session: URLSession
|
||||
let webAuthSessionType: WebAuthSession.Type
|
||||
let keychain: Keychain.Type
|
||||
let userDefaults: UserDefaults
|
||||
let userNotificationClient: UserNotificationClient
|
||||
let reduceMotion: () -> Bool
|
||||
let uuid: () -> UUID
|
||||
let inMemoryContent: Bool
|
||||
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()
|
||||
}
|
||||
|
||||
func authenticatedIdentitiesPublisher() -> AnyPublisher<[Identity], Error> {
|
||||
database.authenticatedIdentitiesPublisher()
|
||||
}
|
||||
|
||||
func mostRecentAuthenticatedIdentity() throws -> Identity? {
|
||||
try database.mostRecentAuthenticatedIdentity()
|
||||
}
|
||||
|
||||
func createIdentity(url: URL, kind: IdentityCreation) -> AnyPublisher<Never, Error> {
|
||||
let id = environment.uuid()
|
||||
let secrets = Secrets(identityId: id, keychain: environment.keychain)
|
||||
|
|
|
@ -228,10 +228,6 @@ public extension IdentityService {
|
|||
func domainBlocksService() -> DomainBlocksService {
|
||||
DomainBlocksService(mastodonAPIClient: mastodonAPIClient)
|
||||
}
|
||||
|
||||
func newStatusService() -> NewStatusService {
|
||||
NewStatusService(id: id, identityDatabase: identityDatabase, environment: environment)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ShareExtensionError: Error {
|
||||
case noAccountFound
|
||||
}
|
||||
import ViewModels
|
||||
|
||||
extension ShareExtensionError: LocalizedError {
|
||||
var errorDescription: String? {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .noAccountFound:
|
||||
return NSLocalizedString("share-extension-error.no-account-found", comment: "")
|
|
@ -7,20 +7,26 @@ import ViewModels
|
|||
|
||||
@objc(ShareExtensionNavigationViewController)
|
||||
class ShareExtensionNavigationViewController: UINavigationController {
|
||||
private let viewModel = ShareExtensionNavigationViewModel(
|
||||
environment: .live(
|
||||
userNotificationCenter: .current(),
|
||||
reduceMotion: { UIAccessibility.isReduceMotionEnabled }))
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
||||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||
|
||||
let viewModel: NewStatusViewModel
|
||||
let newStatusViewModel: NewStatusViewModel
|
||||
|
||||
do {
|
||||
viewModel = try newStatusViewModel()
|
||||
newStatusViewModel = try viewModel.newStatusViewModel()
|
||||
} catch {
|
||||
setViewControllers([ShareErrorViewController(error: error)], animated: false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setViewControllers([NewStatusViewController(viewModel: viewModel)], animated: false)
|
||||
setViewControllers(
|
||||
[NewStatusViewController(viewModel: newStatusViewModel, isShareExtension: true)],
|
||||
animated: false)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
|
@ -28,23 +34,3 @@ class ShareExtensionNavigationViewController: UINavigationController {
|
|||
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.
|
||||
|
||||
import Combine
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
import ViewModels
|
||||
|
||||
class NewStatusViewController: UIViewController {
|
||||
class NewStatusViewController: UICollectionViewController {
|
||||
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.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)
|
||||
|
@ -20,18 +40,103 @@ class NewStatusViewController: UIViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
collectionView.dataSource = dataSource
|
||||
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
navigationItem.leftBarButtonItem = .init(
|
||||
systemItem: .close,
|
||||
primaryAction: UIAction { [weak self] _ in self?.extensionContext?.completeRequest(returningItems: nil) })
|
||||
setupBarButtonItems(identification: viewModel.identification)
|
||||
|
||||
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?) {
|
||||
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,
|
||||
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 {
|
||||
static let preview = NewStatusViewModel(
|
||||
service: .init(
|
||||
id: identityId,
|
||||
identityDatabase: db,
|
||||
environment: environment))
|
||||
static let preview = RootViewModel.preview.newStatusViewModel(identification: .preview)
|
||||
}
|
||||
|
||||
// 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),
|
||||
identification: identification)
|
||||
}
|
||||
|
||||
func newStatusViewModel() -> NewStatusViewModel {
|
||||
NewStatusViewModel(service: identification.service.newStatusService())
|
||||
}
|
||||
}
|
||||
|
||||
extension NavigationViewModel.Tab: Identifiable {
|
||||
|
|
|
@ -6,9 +6,53 @@ import Mastodon
|
|||
import ServiceLayer
|
||||
|
||||
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) {
|
||||
self.service = service
|
||||
private let allIdentitiesService: AllIdentitiesService
|
||||
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,
|
||||
instanceURLService: InstanceURLService(environment: environment))
|
||||
}
|
||||
|
||||
func newStatusViewModel(identification: Identification) -> NewStatusViewModel {
|
||||
NewStatusViewModel(
|
||||
allIdentitiesService: allIdentitiesService,
|
||||
identification: identification,
|
||||
environment: environment)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
func makeUIViewController(context: Context) -> NewStatusViewController {
|
||||
NewStatusViewController(viewModel: viewModelClosure())
|
||||
NewStatusViewController(viewModel: viewModelClosure(), isShareExtension: false)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: NewStatusViewController, context: Context) {
|
||||
|
|
|
@ -41,9 +41,11 @@ struct TabNavigationView: View {
|
|||
EmptyView()
|
||||
.fullScreenCover(isPresented: $viewModel.presentingNewStatus) {
|
||||
NavigationView {
|
||||
NewStatusView(viewModelClosure: viewModel.newStatusViewModel)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
NewStatusView {
|
||||
rootViewModel.newStatusViewModel(identification: viewModel.identification)
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.environmentObject(viewModel)
|
||||
|
@ -137,7 +139,9 @@ private extension TabNavigationView {
|
|||
viewModel.presentingSecondaryNavigation.toggle()
|
||||
} label: {
|
||||
KFImage(viewModel.identification.identity.image,
|
||||
options: .downsampled(dimension: 28, scaleFactor: displayScale))
|
||||
options: .downsampled(
|
||||
dimension: .barButtonItemDimension,
|
||||
scaleFactor: displayScale))
|
||||
.placeholder { Image(systemName: "gear") }
|
||||
.renderingMode(.original)
|
||||
.contextMenu(ContextMenu {
|
||||
|
@ -149,7 +153,9 @@ private extension TabNavigationView {
|
|||
title: { Text(recentIdentity.handle) },
|
||||
icon: {
|
||||
KFImage(recentIdentity.image,
|
||||
options: .downsampled(dimension: 28, scaleFactor: displayScale))
|
||||
options: .downsampled(
|
||||
dimension: .barButtonItemDimension,
|
||||
scaleFactor: displayScale))
|
||||
.renderingMode(.original)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ extension CGFloat {
|
|||
static let avatarDimension: Self = 50
|
||||
static let hairline = 1 / UIScreen.main.scale
|
||||
static let minimumButtonDimension: Self = 44
|
||||
static let barButtonItemDimension: Self = 28
|
||||
}
|
||||
|
||||
extension TimeInterval {
|
||||
|
|
Loading…
Reference in a new issue