mirror of
https://github.com/metabolist/metatext.git
synced 2024-12-23 05:56:30 +00:00
Refactoring
This commit is contained in:
parent
39a7b24370
commit
43ccc12468
10 changed files with 93 additions and 118 deletions
|
@ -38,7 +38,8 @@ extension IdentityDatabase {
|
||||||
lastUsedAt: Date(),
|
lastUsedAt: Date(),
|
||||||
preferences: Identity.Preferences(),
|
preferences: Identity.Preferences(),
|
||||||
instanceURI: nil,
|
instanceURI: nil,
|
||||||
pushSubscriptionAlerts: nil)
|
lastRegisteredDeviceToken: nil,
|
||||||
|
pushSubscriptionAlerts: .initial)
|
||||||
.save)
|
.save)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -202,7 +203,7 @@ private extension IdentityDatabase {
|
||||||
.indexed()
|
.indexed()
|
||||||
.references("instance", column: "uri")
|
.references("instance", column: "uri")
|
||||||
t.column("preferences", .blob).notNull()
|
t.column("preferences", .blob).notNull()
|
||||||
t.column("pushSubscriptionAlerts", .blob)
|
t.column("pushSubscriptionAlerts", .blob).notNull()
|
||||||
t.column("lastRegisteredDeviceToken", .text)
|
t.column("lastRegisteredDeviceToken", .text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +234,8 @@ private struct StoredIdentity: Codable, Hashable, TableRecord, FetchableRecord,
|
||||||
let lastUsedAt: Date
|
let lastUsedAt: Date
|
||||||
let preferences: Identity.Preferences
|
let preferences: Identity.Preferences
|
||||||
let instanceURI: String?
|
let instanceURI: String?
|
||||||
let pushSubscriptionAlerts: PushSubscription.Alerts?
|
let lastRegisteredDeviceToken: String?
|
||||||
|
let pushSubscriptionAlerts: PushSubscription.Alerts
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StoredIdentity {
|
extension StoredIdentity {
|
||||||
|
@ -253,7 +255,7 @@ private struct IdentityResult: Codable, Hashable, FetchableRecord {
|
||||||
let identity: StoredIdentity
|
let identity: StoredIdentity
|
||||||
let instance: Identity.Instance?
|
let instance: Identity.Instance?
|
||||||
let account: Identity.Account?
|
let account: Identity.Account?
|
||||||
let pushSubscriptionAlerts: PushSubscription.Alerts?
|
let pushSubscriptionAlerts: PushSubscription.Alerts
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Identity {
|
private extension Identity {
|
||||||
|
@ -265,6 +267,7 @@ private extension Identity {
|
||||||
preferences: result.identity.preferences,
|
preferences: result.identity.preferences,
|
||||||
instance: result.instance,
|
instance: result.instance,
|
||||||
account: result.account,
|
account: result.account,
|
||||||
|
lastRegisteredDeviceToken: result.identity.lastRegisteredDeviceToken,
|
||||||
pushSubscriptionAlerts: result.pushSubscriptionAlerts)
|
pushSubscriptionAlerts: result.pushSubscriptionAlerts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@ struct Identity: Codable, Hashable, Identifiable {
|
||||||
let preferences: Identity.Preferences
|
let preferences: Identity.Preferences
|
||||||
let instance: Identity.Instance?
|
let instance: Identity.Instance?
|
||||||
let account: Identity.Account?
|
let account: Identity.Account?
|
||||||
let pushSubscriptionAlerts: PushSubscription.Alerts?
|
let lastRegisteredDeviceToken: String?
|
||||||
|
let pushSubscriptionAlerts: PushSubscription.Alerts
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Identity {
|
extension Identity {
|
||||||
|
|
|
@ -15,3 +15,7 @@ struct PushSubscription: Codable {
|
||||||
let alerts: Alerts
|
let alerts: Alerts
|
||||||
let serverKey: String
|
let serverKey: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PushSubscription.Alerts {
|
||||||
|
static let initial: Self = Self(follow: true, favourite: true, reblog: true, mention: true, poll: true)
|
||||||
|
}
|
||||||
|
|
|
@ -7,13 +7,9 @@ enum PushSubscriptionEndpoint {
|
||||||
endpoint: URL,
|
endpoint: URL,
|
||||||
publicKey: String,
|
publicKey: String,
|
||||||
auth: String,
|
auth: String,
|
||||||
follow: Bool,
|
alerts: PushSubscription.Alerts)
|
||||||
favourite: Bool,
|
|
||||||
reblog: Bool,
|
|
||||||
mention: Bool,
|
|
||||||
poll: Bool)
|
|
||||||
case read
|
case read
|
||||||
case update(follow: Bool, favourite: Bool, reblog: Bool, mention: Bool, poll: Bool)
|
case update(alerts: PushSubscription.Alerts)
|
||||||
case delete
|
case delete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +33,7 @@ extension PushSubscriptionEndpoint: MastodonEndpoint {
|
||||||
|
|
||||||
var parameters: [String: Any]? {
|
var parameters: [String: Any]? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .create(endpoint, publicKey, auth, follow, favourite, reblog, mention, poll):
|
case let .create(endpoint, publicKey, auth, alerts):
|
||||||
return ["subscription":
|
return ["subscription":
|
||||||
["endpoint": endpoint.absoluteString,
|
["endpoint": endpoint.absoluteString,
|
||||||
"keys": [
|
"keys": [
|
||||||
|
@ -45,20 +41,20 @@ extension PushSubscriptionEndpoint: MastodonEndpoint {
|
||||||
"auth": auth]],
|
"auth": auth]],
|
||||||
"data": [
|
"data": [
|
||||||
"alerts": [
|
"alerts": [
|
||||||
"follow": follow,
|
"follow": alerts.follow,
|
||||||
"favourite": favourite,
|
"favourite": alerts.favourite,
|
||||||
"reblog": reblog,
|
"reblog": alerts.reblog,
|
||||||
"mention": mention,
|
"mention": alerts.mention,
|
||||||
"poll": poll
|
"poll": alerts.poll
|
||||||
]]]
|
]]]
|
||||||
case let .update(follow, favourite, reblog, mention, poll):
|
case let .update(alerts):
|
||||||
return ["data":
|
return ["data":
|
||||||
["alerts":
|
["alerts":
|
||||||
["follow": follow,
|
["follow": alerts.follow,
|
||||||
"favourite": favourite,
|
"favourite": alerts.favourite,
|
||||||
"reblog": reblog,
|
"reblog": alerts.reblog,
|
||||||
"mention": mention,
|
"mention": alerts.mention,
|
||||||
"poll": poll]]]
|
"poll": alerts.poll]]]
|
||||||
default: return nil
|
default: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,82 +65,20 @@ extension IdentitiesService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePushSubscription(
|
|
||||||
identityID: UUID,
|
|
||||||
instanceURL: URL,
|
|
||||||
deviceToken: String,
|
|
||||||
alerts: PushSubscription.Alerts?) -> AnyPublisher<Void, Error> {
|
|
||||||
let secretsService = SecretsService(
|
|
||||||
identityID: identityID,
|
|
||||||
keychainServiceType: environment.keychainServiceType)
|
|
||||||
let accessTokenOptional: String?
|
|
||||||
|
|
||||||
do {
|
|
||||||
accessTokenOptional = try secretsService.item(.accessToken) as String?
|
|
||||||
} catch {
|
|
||||||
return Fail(error: error).eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let accessToken: String = accessTokenOptional
|
|
||||||
else { return Empty().eraseToAnyPublisher() }
|
|
||||||
|
|
||||||
let publicKey: String
|
|
||||||
let auth: String
|
|
||||||
|
|
||||||
do {
|
|
||||||
publicKey = try secretsService.generatePushKeyAndReturnPublicKey().base64EncodedString()
|
|
||||||
auth = try secretsService.generatePushAuth().base64EncodedString()
|
|
||||||
} catch {
|
|
||||||
return Fail(error: error).eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
let networkClient = MastodonClient(session: environment.session)
|
|
||||||
networkClient.instanceURL = instanceURL
|
|
||||||
networkClient.accessToken = accessToken
|
|
||||||
|
|
||||||
let endpoint = Self.pushSubscriptionEndpointURL
|
|
||||||
.appendingPathComponent(deviceToken)
|
|
||||||
.appendingPathComponent(identityID.uuidString)
|
|
||||||
|
|
||||||
return networkClient.request(
|
|
||||||
PushSubscriptionEndpoint.create(
|
|
||||||
endpoint: endpoint,
|
|
||||||
publicKey: publicKey,
|
|
||||||
auth: auth,
|
|
||||||
follow: alerts?.follow ?? true,
|
|
||||||
favourite: alerts?.favourite ?? true,
|
|
||||||
reblog: alerts?.reblog ?? true,
|
|
||||||
mention: alerts?.mention ?? true,
|
|
||||||
poll: alerts?.poll ?? true))
|
|
||||||
.map { (deviceToken, $0.alerts, identityID) }
|
|
||||||
.flatMap(identityDatabase.updatePushSubscription(deviceToken:alerts:forIdentityID:))
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePushSubscriptions(deviceToken: String) -> AnyPublisher<Void, Error> {
|
func updatePushSubscriptions(deviceToken: String) -> AnyPublisher<Void, Error> {
|
||||||
identityDatabase.identitiesWithOutdatedDeviceTokens(deviceToken: deviceToken)
|
identityDatabase.identitiesWithOutdatedDeviceTokens(deviceToken: deviceToken)
|
||||||
.flatMap { identities -> Publishers.MergeMany<AnyPublisher<Void, Never>> in
|
.tryMap { [weak self] identities -> [AnyPublisher<Void, Never>] in
|
||||||
Publishers.MergeMany(
|
guard let self = self else { return [Empty().eraseToAnyPublisher()] }
|
||||||
identities.map { [weak self] in
|
|
||||||
guard let self = self else { return Empty().eraseToAnyPublisher() }
|
|
||||||
|
|
||||||
return self.updatePushSubscription(
|
return try identities.map {
|
||||||
identityID: $0.id,
|
try self.identityService(id: $0.id)
|
||||||
instanceURL: $0.url,
|
.createPushSubscription(deviceToken: deviceToken, alerts: $0.pushSubscriptionAlerts)
|
||||||
deviceToken: deviceToken,
|
.catch { _ in Empty() } // don't want to disrupt pipeline, consider future telemetry
|
||||||
alerts: $0.pushSubscriptionAlerts)
|
|
||||||
.catch { _ in Empty() } // can't let one failure stop the pipeline
|
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.map(Publishers.MergeMany.init)
|
||||||
|
.map { _ in () }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension IdentitiesService {
|
|
||||||
#if DEBUG
|
|
||||||
static let pushSubscriptionEndpointURL = URL(string: "https://metatext-apns.metabolist.com/push?sandbox=true")!
|
|
||||||
#else
|
|
||||||
static let pushSubscriptionEndpointURL = URL(string: "https://metatext-apns.metabolist.com/push")!
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ class IdentityService {
|
||||||
private let identityDatabase: IdentityDatabase
|
private let identityDatabase: IdentityDatabase
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
private let networkClient: MastodonClient
|
private let networkClient: MastodonClient
|
||||||
|
private let secretsService: SecretsService
|
||||||
private let observationErrorsInput = PassthroughSubject<Error, Never>()
|
private let observationErrorsInput = PassthroughSubject<Error, Never>()
|
||||||
|
|
||||||
init(identityID: UUID,
|
init(identityID: UUID,
|
||||||
|
@ -29,12 +30,12 @@ class IdentityService {
|
||||||
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
|
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
|
||||||
|
|
||||||
self.identity = identity
|
self.identity = identity
|
||||||
networkClient = MastodonClient(session: environment.session)
|
secretsService = SecretsService(
|
||||||
networkClient.instanceURL = identity.url
|
|
||||||
networkClient.accessToken = try SecretsService(
|
|
||||||
identityID: identityID,
|
identityID: identityID,
|
||||||
keychainServiceType: environment.keychainServiceType)
|
keychainServiceType: environment.keychainServiceType)
|
||||||
.item(.accessToken)
|
networkClient = MastodonClient(session: environment.session)
|
||||||
|
networkClient.instanceURL = identity.url
|
||||||
|
networkClient.accessToken = try secretsService.item(.accessToken)
|
||||||
|
|
||||||
observation.catch { [weak self] error -> Empty<Identity, Never> in
|
observation.catch { [weak self] error -> Empty<Identity, Never> in
|
||||||
self?.observationErrorsInput.send(error)
|
self?.observationErrorsInput.send(error)
|
||||||
|
@ -85,4 +86,39 @@ extension IdentityService {
|
||||||
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Void, Error> {
|
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Void, Error> {
|
||||||
identityDatabase.updatePreferences(preferences, forIdentityID: identity.id)
|
identityDatabase.updatePreferences(preferences, forIdentityID: identity.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createPushSubscription(deviceToken: String, alerts: PushSubscription.Alerts) -> AnyPublisher<Void, Error> {
|
||||||
|
let publicKey: String
|
||||||
|
let auth: String
|
||||||
|
|
||||||
|
do {
|
||||||
|
publicKey = try secretsService.generatePushKeyAndReturnPublicKey().base64EncodedString()
|
||||||
|
auth = try secretsService.generatePushAuth().base64EncodedString()
|
||||||
|
} catch {
|
||||||
|
return Fail(error: error).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
let identityID = identity.id
|
||||||
|
let endpoint = Self.pushSubscriptionEndpointURL
|
||||||
|
.appendingPathComponent(deviceToken)
|
||||||
|
.appendingPathComponent(identityID.uuidString)
|
||||||
|
|
||||||
|
return networkClient.request(
|
||||||
|
PushSubscriptionEndpoint.create(
|
||||||
|
endpoint: endpoint,
|
||||||
|
publicKey: publicKey,
|
||||||
|
auth: auth,
|
||||||
|
alerts: alerts))
|
||||||
|
.map { (deviceToken, $0.alerts, identityID) }
|
||||||
|
.flatMap(identityDatabase.updatePushSubscription(deviceToken:alerts:forIdentityID:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension IdentityService {
|
||||||
|
#if DEBUG
|
||||||
|
static let pushSubscriptionEndpointURL = URL(string: "https://metatext-apns.metabolist.com/push?sandbox=true")!
|
||||||
|
#else
|
||||||
|
static let pushSubscriptionEndpointURL = URL(string: "https://metatext-apns.metabolist.com/push")!
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,15 +7,15 @@ class AddIdentityViewModel: ObservableObject {
|
||||||
@Published var urlFieldText = ""
|
@Published var urlFieldText = ""
|
||||||
@Published var alertItem: AlertItem?
|
@Published var alertItem: AlertItem?
|
||||||
@Published private(set) var loading = false
|
@Published private(set) var loading = false
|
||||||
let addedIdentityIDAndURL: AnyPublisher<(UUID, URL), Never>
|
let addedIdentityID: AnyPublisher<UUID, Never>
|
||||||
|
|
||||||
private let identitiesService: IdentitiesService
|
private let identitiesService: IdentitiesService
|
||||||
private let addedIdentityIDAndURLInput = PassthroughSubject<(UUID, URL), Never>()
|
private let addedIdentityIDInput = PassthroughSubject<UUID, Never>()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(identitiesService: IdentitiesService) {
|
init(identitiesService: IdentitiesService) {
|
||||||
self.identitiesService = identitiesService
|
self.identitiesService = identitiesService
|
||||||
addedIdentityIDAndURL = addedIdentityIDAndURLInput.eraseToAnyPublisher()
|
addedIdentityID = addedIdentityIDInput.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func logInTapped() {
|
func logInTapped() {
|
||||||
|
@ -33,13 +33,13 @@ class AddIdentityViewModel: ObservableObject {
|
||||||
identitiesService.authorizeIdentity(id: identityID, instanceURL: instanceURL)
|
identitiesService.authorizeIdentity(id: identityID, instanceURL: instanceURL)
|
||||||
.map { (identityID, instanceURL) }
|
.map { (identityID, instanceURL) }
|
||||||
.flatMap(identitiesService.createIdentity(id:instanceURL:))
|
.flatMap(identitiesService.createIdentity(id:instanceURL:))
|
||||||
.map { (identityID, instanceURL) }
|
.map { identityID }
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
receiveSubscription: { [weak self] _ in self?.loading = true },
|
receiveSubscription: { [weak self] _ in self?.loading = true },
|
||||||
receiveCompletion: { [weak self] _ in self?.loading = false })
|
receiveCompletion: { [weak self] _ in self?.loading = false })
|
||||||
.sink(receiveValue: addedIdentityIDAndURLInput.send)
|
.sink(receiveValue: addedIdentityIDInput.send)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,9 +57,9 @@ class AddIdentityViewModel: ObservableObject {
|
||||||
|
|
||||||
// TODO: Ensure instance has not disabled public preview
|
// TODO: Ensure instance has not disabled public preview
|
||||||
identitiesService.createIdentity(id: identityID, instanceURL: instanceURL)
|
identitiesService.createIdentity(id: identityID, instanceURL: instanceURL)
|
||||||
.map { (identityID, instanceURL) }
|
.map { identityID }
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink(receiveValue: addedIdentityIDAndURLInput.send)
|
.sink(receiveValue: addedIdentityIDInput.send)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,22 +55,19 @@ extension RootViewModel {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
identityService.updateLastUse()
|
identityService.updateLastUse()
|
||||||
.sink(receiveCompletion: { _ in }, receiveValue: {})
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
mainNavigationViewModel = MainNavigationViewModel(identityService: identityService)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newIdentityCreated(id: UUID, instanceURL: URL) {
|
|
||||||
newIdentitySelected(id: id)
|
|
||||||
|
|
||||||
userNotificationService.isAuthorized()
|
userNotificationService.isAuthorized()
|
||||||
.filter { $0 }
|
.filter { $0 }
|
||||||
.zip(appDelegate.registerForRemoteNotifications())
|
.zip(appDelegate.registerForRemoteNotifications())
|
||||||
.map { (id, instanceURL, $1, nil) }
|
.filter { identityService.identity.lastRegisteredDeviceToken != $1 }
|
||||||
.flatMap(identitiesService.updatePushSubscription(identityID:instanceURL:deviceToken:alerts:))
|
.map { ($1, identityService.identity.pushSubscriptionAlerts) }
|
||||||
|
.flatMap(identityService.createPushSubscription(deviceToken:alerts:))
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
mainNavigationViewModel = MainNavigationViewModel(identityService: identityService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentity(id: UUID) {
|
func deleteIdentity(id: UUID) {
|
||||||
|
|
|
@ -34,9 +34,9 @@ struct AddIdentityView: View {
|
||||||
}
|
}
|
||||||
.paddingIfMac()
|
.paddingIfMac()
|
||||||
.alertItem($viewModel.alertItem)
|
.alertItem($viewModel.alertItem)
|
||||||
.onReceive(viewModel.addedIdentityIDAndURL) { id, url in
|
.onReceive(viewModel.addedIdentityID) { id in
|
||||||
withAnimation {
|
withAnimation {
|
||||||
rootViewModel.newIdentityCreated(id: id, instanceURL: url)
|
rootViewModel.newIdentitySelected(id: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ class RootViewModelTests: XCTestCase {
|
||||||
identitiesService: IdentitiesService(
|
identitiesService: IdentitiesService(
|
||||||
identityDatabase: .fresh(),
|
identityDatabase: .fresh(),
|
||||||
environment: .development),
|
environment: .development),
|
||||||
notificationService: NotificationService())
|
userNotificationService: UserNotificationService())
|
||||||
let recorder = sut.$mainNavigationViewModel.record()
|
let recorder = sut.$mainNavigationViewModel.record()
|
||||||
|
|
||||||
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||||
|
|
Loading…
Reference in a new issue