From 888604497c18c330ba0b411e66f30a04b968f007 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Fri, 14 Aug 2020 14:41:55 -0700 Subject: [PATCH] Notification type preferences --- Development Assets/DevelopmentModels.swift | 4 ++ Metatext.xcodeproj/project.pbxproj | 12 +++++ Shared/Databases/IdentityDatabase.swift | 12 ++--- Shared/Localizations/Localizable.strings | 6 +++ Shared/Model/PushSubscription.swift | 10 ++--- Shared/Services/IdentityService.swift | 13 +++++- ...otificationTypesPreferencesViewModel.swift | 44 +++++++++++++++++++ Shared/View Models/PreferencesViewModel.swift | 4 ++ .../NotificationTypesPreferencesView.swift | 32 ++++++++++++++ Shared/Views/PreferencesView.swift | 3 ++ 10 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 Shared/View Models/NotificationTypesPreferencesViewModel.swift create mode 100644 Shared/Views/NotificationTypesPreferencesView.swift diff --git a/Development Assets/DevelopmentModels.swift b/Development Assets/DevelopmentModels.swift index 09330b4..9ef14d6 100644 --- a/Development Assets/DevelopmentModels.swift +++ b/Development Assets/DevelopmentModels.swift @@ -106,4 +106,8 @@ extension PostingReadingPreferencesViewModel { static let development = PostingReadingPreferencesViewModel(identityService: .development) } +extension NotificationTypesPreferencesViewModel { + static let development = NotificationTypesPreferencesViewModel(identityService: .development) +} + // swiftlint:enable force_try diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 38ff6d1..589cc00 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -86,6 +86,10 @@ D074577824D29006004758DB /* MockWebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* MockWebAuthSession.swift */; }; D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; }; D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; }; + D075817924E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D075817824E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift */; }; + D075817A24E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D075817824E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift */; }; + D075817C24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D075817B24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift */; }; + D075817D24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D075817B24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift */; }; D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; }; D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; }; D0A1CA7424DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */; }; @@ -266,6 +270,8 @@ D06B491E24D3F7FE00642749 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; D074577624D29006004758DB /* MockWebAuthSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWebAuthSession.swift; sourceTree = ""; }; D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = ""; }; + D075817824E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTypesPreferencesViewModel.swift; sourceTree = ""; }; + D075817B24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTypesPreferencesView.swift; sourceTree = ""; }; D081A40424D0F1A8001B016E /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = ""; }; D0A652AC24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreferencesEndpoint+Stubbing.swift"; sourceTree = ""; }; @@ -545,6 +551,7 @@ isa = PBXGroup; children = ( D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */, + D075817B24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift */, D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */, D0091B6D24DD68090040E8D2 /* PreferencesView.swift */, D0BEC93A24C96FD500E864C4 /* RootView.swift */, @@ -568,6 +575,7 @@ isa = PBXGroup; children = ( D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */, + D075817824E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift */, D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */, D0091B7024DD68220040E8D2 /* PreferencesViewModel.swift */, D0BEC93724C9632800E864C4 /* RootViewModel.swift */, @@ -884,6 +892,7 @@ D04FD73924D4A7B4007D572D /* AccountEndpoint+Stubbing.swift in Sources */, D0DB6F0924C65AC000D965FE /* AddIdentityViewModel.swift in Sources */, D0CD847324DBDEC700CF380C /* MastodonPreferences.swift in Sources */, + D075817924E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift in Sources */, D0ED1BD724CF94B200B4899C /* Application.swift in Sources */, D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */, D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */, @@ -893,6 +902,7 @@ D0EC8DE824E21FEC00A08489 /* Data+Extensions.swift in Sources */, D0666A6324C6DC6C00F3F04B /* AppAuthorization.swift in Sources */, D019E6E524DF72E700697C7D /* AccountEndpoint.swift in Sources */, + D075817C24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift in Sources */, D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */, D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */, D0159F8A24DE742F00E78478 /* IdentitiesViewModel.swift in Sources */, @@ -1029,6 +1039,7 @@ D0A1CA7524DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */, D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */, D0C963FF24CC3812003BD330 /* Publisher+Extensions.swift in Sources */, + D075817D24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift in Sources */, D019E6E824DF72E700697C7D /* AccessTokenEndpoint.swift in Sources */, D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */, D0DC175C24D0154F00A75C65 /* MastodonAPI.swift in Sources */, @@ -1038,6 +1049,7 @@ D0666A7024C6DFB300F3F04B /* AccessToken.swift in Sources */, D0ED1BCC24CF744200B4899C /* MastodonClient.swift in Sources */, D0091B6924DC10B30040E8D2 /* PostingReadingPreferencesView.swift in Sources */, + D075817A24E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift in Sources */, D019E6E624DF72E700697C7D /* AccountEndpoint.swift in Sources */, D0CD847724DBDF3C00CF380C /* Status.swift in Sources */, D0EC8DEF24E2704D00A08489 /* PushSubscriptionEndpoint.swift in Sources */, diff --git a/Shared/Databases/IdentityDatabase.swift b/Shared/Databases/IdentityDatabase.swift index 28cdd80..a2947aa 100644 --- a/Shared/Databases/IdentityDatabase.swift +++ b/Shared/Databases/IdentityDatabase.swift @@ -103,8 +103,8 @@ extension IdentityDatabase { .eraseToAnyPublisher() } - func updatePushSubscription(deviceToken: String, - alerts: PushSubscription.Alerts, + func updatePushSubscription(alerts: PushSubscription.Alerts, + deviceToken: String? = nil, forIdentityID identityID: UUID) -> AnyPublisher { databaseQueue.writePublisher { let data = try StoredIdentity.databaseJSONEncoder(for: "pushSubscriptionAlerts").encode(alerts) @@ -113,9 +113,11 @@ extension IdentityDatabase { .filter(Column("id") == identityID) .updateAll($0, Column("pushSubscriptionAlerts").set(to: data)) - try StoredIdentity - .filter(Column("id") == identityID) - .updateAll($0, Column("lastRegisteredDeviceToken").set(to: deviceToken)) + if let deviceToken = deviceToken { + try StoredIdentity + .filter(Column("id") == identityID) + .updateAll($0, Column("lastRegisteredDeviceToken").set(to: deviceToken)) + } } .eraseToAnyPublisher() } diff --git a/Shared/Localizations/Localizable.strings b/Shared/Localizations/Localizable.strings index 5b1a923..e2b27ed 100644 --- a/Shared/Localizations/Localizable.strings +++ b/Shared/Localizations/Localizable.strings @@ -20,6 +20,12 @@ "preferences.expand-media.show-all" = "Show all"; "preferences.expand-media.hide-all" = "Hide all"; "preferences.reading-expand-spoilers" = "Always expand content warnings"; +"preferences.notification-types" = "Notification Types"; +"preferences.notification-types.follow" = "Follow"; +"preferences.notification-types.favourite" = "Favorite"; +"preferences.notification-types.reblog" = "Reblog"; +"preferences.notification-types.mention" = "Mention"; +"preferences.notification-types.poll" = "Poll"; "status.visibility.public" = "Public"; "status.visibility.unlisted" = "Unlisted"; "status.visibility.private" = "Private"; diff --git a/Shared/Model/PushSubscription.swift b/Shared/Model/PushSubscription.swift index e3b05c5..cfedd5d 100644 --- a/Shared/Model/PushSubscription.swift +++ b/Shared/Model/PushSubscription.swift @@ -4,11 +4,11 @@ import Foundation struct PushSubscription: Codable { struct Alerts: Codable, Hashable { - let follow: Bool - let favourite: Bool - let reblog: Bool - let mention: Bool - let poll: Bool + var follow: Bool + var favourite: Bool + var reblog: Bool + var mention: Bool + var poll: Bool } let endpoint: URL diff --git a/Shared/Services/IdentityService.swift b/Shared/Services/IdentityService.swift index 1208ae5..069924e 100644 --- a/Shared/Services/IdentityService.swift +++ b/Shared/Services/IdentityService.swift @@ -109,8 +109,17 @@ extension IdentityService { publicKey: publicKey, auth: auth, alerts: alerts)) - .map { (deviceToken, $0.alerts, identityID) } - .flatMap(identityDatabase.updatePushSubscription(deviceToken:alerts:forIdentityID:)) + .map { ($0.alerts, deviceToken, identityID) } + .flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:forIdentityID:)) + .eraseToAnyPublisher() + } + + func updatePushSubscription(alerts: PushSubscription.Alerts) -> AnyPublisher { + let identityID = identity.id + + return networkClient.request(PushSubscriptionEndpoint.update(alerts: alerts)) + .map { ($0.alerts, nil, identityID) } + .flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:forIdentityID:)) .eraseToAnyPublisher() } } diff --git a/Shared/View Models/NotificationTypesPreferencesViewModel.swift b/Shared/View Models/NotificationTypesPreferencesViewModel.swift new file mode 100644 index 0000000..2520efd --- /dev/null +++ b/Shared/View Models/NotificationTypesPreferencesViewModel.swift @@ -0,0 +1,44 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import Combine + +class NotificationTypesPreferencesViewModel: ObservableObject { + @Published var pushSubscriptionAlerts: PushSubscription.Alerts + @Published var alertItem: AlertItem? + + private let identityService: IdentityService + private var cancellables = Set() + + init(identityService: IdentityService) { + self.identityService = identityService + pushSubscriptionAlerts = identityService.identity.pushSubscriptionAlerts + + identityService.$identity + .map(\.pushSubscriptionAlerts) + .dropFirst() + .removeDuplicates() + .assign(to: &$pushSubscriptionAlerts) + + $pushSubscriptionAlerts + .dropFirst() + .removeDuplicates() + .sink(receiveValue: update(alerts:)) + .store(in: &cancellables) + } +} + +private extension NotificationTypesPreferencesViewModel { + func update(alerts: PushSubscription.Alerts) { + guard alerts != identityService.identity.pushSubscriptionAlerts else { return } + + identityService.updatePushSubscription(alerts: alerts) + .sink { [weak self] in + guard let self = self, case let .failure(error) = $0 else { return } + + self.alertItem = AlertItem(error: error) + self.pushSubscriptionAlerts = self.identityService.identity.pushSubscriptionAlerts + } receiveValue: {} + .store(in: &cancellables) + } +} diff --git a/Shared/View Models/PreferencesViewModel.swift b/Shared/View Models/PreferencesViewModel.swift index b6a31da..28dfbd8 100644 --- a/Shared/View Models/PreferencesViewModel.swift +++ b/Shared/View Models/PreferencesViewModel.swift @@ -17,4 +17,8 @@ extension PreferencesViewModel { func postingReadingPreferencesViewModel() -> PostingReadingPreferencesViewModel { PostingReadingPreferencesViewModel(identityService: identityService) } + + func notificationTypesPreferencesViewModel() -> NotificationTypesPreferencesViewModel { + NotificationTypesPreferencesViewModel(identityService: identityService) + } } diff --git a/Shared/Views/NotificationTypesPreferencesView.swift b/Shared/Views/NotificationTypesPreferencesView.swift new file mode 100644 index 0000000..9962a26 --- /dev/null +++ b/Shared/Views/NotificationTypesPreferencesView.swift @@ -0,0 +1,32 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import SwiftUI + +struct NotificationTypesPreferencesView: View { + @StateObject var viewModel: NotificationTypesPreferencesViewModel + + var body: some View { + Form { + Toggle("preferences.notification-types.follow", + isOn: $viewModel.pushSubscriptionAlerts.follow) + Toggle("preferences.notification-types.favourite", + isOn: $viewModel.pushSubscriptionAlerts.favourite) + Toggle("preferences.notification-types.reblog", + isOn: $viewModel.pushSubscriptionAlerts.reblog) + Toggle("preferences.notification-types.mention", + isOn: $viewModel.pushSubscriptionAlerts.mention) + Toggle("preferences.notification-types.poll", + isOn: $viewModel.pushSubscriptionAlerts.poll) + } + .navigationTitle("preferences.notification-types") + .alertItem($viewModel.alertItem) + } +} + +#if DEBUG +struct NotificationTypesPreferencesView_Previews: PreviewProvider { + static var previews: some View { + NotificationTypesPreferencesView(viewModel: .development) + } +} +#endif diff --git a/Shared/Views/PreferencesView.swift b/Shared/Views/PreferencesView.swift index d22ca93..6bd746b 100644 --- a/Shared/Views/PreferencesView.swift +++ b/Shared/Views/PreferencesView.swift @@ -11,6 +11,9 @@ struct PreferencesView: View { NavigationLink("preferences.posting-reading", destination: PostingReadingPreferencesView( viewModel: viewModel.postingReadingPreferencesViewModel())) + NavigationLink("preferences.notification-types", + destination: NotificationTypesPreferencesView( + viewModel: viewModel.notificationTypesPreferencesViewModel())) } } .navigationTitle("preferences")