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