Notification preferences
This commit is contained in:
parent
a566babe3a
commit
98a80e5704
|
@ -228,6 +228,16 @@ public extension IdentityDatabase {
|
|||
|
||||
return Identity(info: info)
|
||||
}
|
||||
|
||||
// Only for use in notification extension
|
||||
func identity(id: Identity.Id) throws -> Identity? {
|
||||
guard let info = try databaseWriter.read(
|
||||
IdentityInfo.request(IdentityRecord.filter(IdentityRecord.Columns.id == id))
|
||||
.fetchOne)
|
||||
else { return nil }
|
||||
|
||||
return Identity(info: info)
|
||||
}
|
||||
}
|
||||
|
||||
private extension IdentityDatabase {
|
||||
|
|
|
@ -186,6 +186,10 @@
|
|||
"preferences.notification-types.mention" = "Mention";
|
||||
"preferences.notification-types.poll" = "Poll";
|
||||
"preferences.notification-types.status" = "Status";
|
||||
"preferences.notifications" = "Notifications";
|
||||
"preferences.notifications.include-account-name" = "Include account name";
|
||||
"preferences.notifications.include-pictures" = "Include pictures";
|
||||
"preferences.notifications.sounds" = "Sounds";
|
||||
"preferences.muted-users" = "Muted Users";
|
||||
"preferences.home-timeline-position-on-startup" = "Home timeline position on startup";
|
||||
"preferences.notifications-position-on-startup" = "Notifications position on startup";
|
||||
|
|
|
@ -38,3 +38,7 @@ public extension MastodonNotification {
|
|||
public static var unknownCase: Self { .unknown }
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonNotification.NotificationType: Identifiable {
|
||||
public var id: Self { self }
|
||||
}
|
||||
|
|
|
@ -3,21 +3,11 @@
|
|||
import Foundation
|
||||
|
||||
public struct PushNotification: Codable {
|
||||
public enum NotificationType: String, Codable, Unknowable {
|
||||
case mention
|
||||
case reblog
|
||||
case favourite
|
||||
case follow
|
||||
case unknown
|
||||
|
||||
public static var unknownCase: Self { .unknown }
|
||||
}
|
||||
|
||||
public let accessToken: String
|
||||
public let body: String
|
||||
public let title: String
|
||||
public let icon: URL
|
||||
public let notificationId: Int
|
||||
public let notificationType: NotificationType
|
||||
public let notificationType: MastodonNotification.NotificationType
|
||||
public let preferredLocale: String
|
||||
}
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D71254246E200B1EBEF /* PollView.swift */; };
|
||||
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D812544D80000B1EBEF /* PollOptionButton.swift */; };
|
||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */; };
|
||||
D08B9F1025CB8E060062D040 /* NotificationPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B9F0F25CB8E060062D040 /* NotificationPreferencesView.swift */; };
|
||||
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */; };
|
||||
D08E52612579D2E100FA2C5F /* DomainBlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */; };
|
||||
D08E5276257C36CA00FA2C5F /* Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D08E526C257C36CA00FA2C5F /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -297,6 +298,7 @@
|
|||
D08B8D71254246E200B1EBEF /* PollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollView.swift; sourceTree = "<group>"; };
|
||||
D08B8D812544D80000B1EBEF /* PollOptionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionButton.swift; sourceTree = "<group>"; };
|
||||
D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollResultView.swift; sourceTree = "<group>"; };
|
||||
D08B9F0F25CB8E060062D040 /* NotificationPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPreferencesView.swift; sourceTree = "<group>"; };
|
||||
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainBlocksView.swift; sourceTree = "<group>"; };
|
||||
D08E526C257C36CA00FA2C5F /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -469,6 +471,7 @@
|
|||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
||||
D08B9F0F25CB8E060062D040 /* NotificationPreferencesView.swift */,
|
||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
||||
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
||||
D0B32F4F250B373600311912 /* RegistrationView.swift */,
|
||||
|
@ -1003,6 +1006,7 @@
|
|||
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */,
|
||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
||||
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
|
||||
D08B9F1025CB8E060062D040 /* NotificationPreferencesView.swift in Sources */,
|
||||
D0E9F9AA258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
||||
D021A60A25C36B32008A0C0D /* IdentityTableViewCell.swift in Sources */,
|
||||
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */,
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.metabolist.metatext</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
|
|
|
@ -29,9 +29,11 @@ final class NotificationService: UNNotificationServiceExtension {
|
|||
guard let bestAttemptContent = bestAttemptContent else { return }
|
||||
|
||||
let pushNotification: PushNotification
|
||||
let decryptedJSON: Data
|
||||
let identityId: Identity.Id
|
||||
|
||||
do {
|
||||
let decryptedJSON = try Self.extractAndDecrypt(userInfo: request.content.userInfo)
|
||||
(decryptedJSON, identityId) = try Self.extractAndDecrypt(userInfo: request.content.userInfo)
|
||||
|
||||
pushNotification = try MastodonDecoder().decode(PushNotification.self, from: decryptedJSON)
|
||||
} catch {
|
||||
|
@ -43,35 +45,23 @@ final class NotificationService: UNNotificationServiceExtension {
|
|||
bestAttemptContent.title = pushNotification.title
|
||||
bestAttemptContent.body = XMLUnescaper(string: pushNotification.body).unescape()
|
||||
|
||||
let fileName = pushNotification.icon.lastPathComponent
|
||||
let fileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(fileName)
|
||||
let appPreferences = AppPreferences(environment: environment)
|
||||
|
||||
KingfisherManager.shared.retrieveImage(with: pushNotification.icon) {
|
||||
switch $0 {
|
||||
case let .success(result):
|
||||
let format: ImageFormat
|
||||
if appPreferences.notificationSounds.contains(pushNotification.notificationType) {
|
||||
bestAttemptContent.sound = .default
|
||||
}
|
||||
|
||||
switch fileURL.pathExtension.lowercased() {
|
||||
case "jpg", "jpeg":
|
||||
format = .JPEG
|
||||
case "gif":
|
||||
format = .GIF
|
||||
case "png":
|
||||
format = .PNG
|
||||
default:
|
||||
format = .unknown
|
||||
}
|
||||
if appPreferences.notificationAccountName,
|
||||
let accountName = try? AllIdentitiesService(environment: environment).identity(id: identityId)?.handle {
|
||||
bestAttemptContent.subtitle = accountName
|
||||
}
|
||||
|
||||
do {
|
||||
try result.image.kf.data(format: format)?.write(to: fileURL)
|
||||
bestAttemptContent.attachments = [try UNNotificationAttachment(identifier: fileName, url: fileURL)]
|
||||
contentHandler(bestAttemptContent)
|
||||
} catch {
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
case .failure:
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
if appPreferences.notificationPictures {
|
||||
Self.addImage(pushNotification: pushNotification,
|
||||
bestAttemptContent: bestAttemptContent,
|
||||
contentHandler: contentHandler)
|
||||
} else {
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,10 +96,10 @@ private extension NotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
static func extractAndDecrypt(userInfo: [AnyHashable: Any]) throws -> Data {
|
||||
static func extractAndDecrypt(userInfo: [AnyHashable: Any]) throws -> (Data, Identity.Id) {
|
||||
guard
|
||||
let identityIdString = userInfo[identityIdUserInfoKey] as? String,
|
||||
let identityId = UUID(uuidString: identityIdString),
|
||||
let identityId = Identity.Id(uuidString: identityIdString),
|
||||
let encryptedMessageBase64 = (userInfo[encryptedMessageUserInfoKey] as? String)?.URLSafeBase64ToBase64(),
|
||||
let encryptedMessage = Data(base64Encoded: encryptedMessageBase64),
|
||||
let saltBase64 = (userInfo[saltUserInfoKey] as? String)?.URLSafeBase64ToBase64(),
|
||||
|
@ -125,11 +115,12 @@ private extension NotificationService {
|
|||
let pushKey = try secretsService.getPushKey()
|
||||
else { throw NotificationServiceError.keychainDataAbsent }
|
||||
|
||||
return try decrypt(encryptedMessage: encryptedMessage,
|
||||
return (try decrypt(encryptedMessage: encryptedMessage,
|
||||
privateKeyData: pushKey,
|
||||
serverPublicKeyData: serverPublicKeyData,
|
||||
auth: auth,
|
||||
salt: salt)
|
||||
salt: salt),
|
||||
identityId)
|
||||
}
|
||||
|
||||
static func decrypt(encryptedMessage: Data,
|
||||
|
@ -179,6 +170,43 @@ private extension NotificationService {
|
|||
|
||||
return Data(unpadded)
|
||||
}
|
||||
|
||||
static func addImage(pushNotification: PushNotification,
|
||||
bestAttemptContent: UNMutableNotificationContent,
|
||||
contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
let fileName = pushNotification.icon.lastPathComponent
|
||||
let fileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
.appendingPathComponent(fileName)
|
||||
|
||||
KingfisherManager.shared.retrieveImage(with: pushNotification.icon) {
|
||||
switch $0 {
|
||||
case let .success(result):
|
||||
let format: ImageFormat
|
||||
|
||||
switch fileURL.pathExtension.lowercased() {
|
||||
case "jpg", "jpeg":
|
||||
format = .JPEG
|
||||
case "gif":
|
||||
format = .GIF
|
||||
case "png":
|
||||
format = .PNG
|
||||
default:
|
||||
format = .unknown
|
||||
}
|
||||
|
||||
do {
|
||||
try result.image.kf.data(format: format)?.write(to: fileURL)
|
||||
bestAttemptContent.attachments =
|
||||
[try UNNotificationAttachment(identifier: fileName, url: fileURL)]
|
||||
contentHandler(bestAttemptContent)
|
||||
} catch {
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
case .failure:
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
|
|
@ -136,6 +136,11 @@ public extension AllIdentitiesService {
|
|||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// Only for use in notification extension
|
||||
func identity(id: Identity.Id) throws -> Identity? {
|
||||
try database.identity(id: id)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AllIdentitiesService.IdentityCreation {
|
||||
|
|
|
@ -140,6 +140,15 @@ public extension AppPreferences {
|
|||
set { self[.defaultEmojiSkinTone] = newValue?.rawValue }
|
||||
}
|
||||
|
||||
var notificationSounds: Set<MastodonNotification.NotificationType> {
|
||||
get {
|
||||
Set((self[.notificationSounds] as [String]?)?.compactMap {
|
||||
MastodonNotification.NotificationType(rawValue: $0)
|
||||
} ?? MastodonNotification.NotificationType.allCasesExceptUnknown)
|
||||
}
|
||||
set { self[.notificationSounds] = newValue.map { $0.rawValue } }
|
||||
}
|
||||
|
||||
var shouldReduceMotion: Bool {
|
||||
systemReduceMotion() && useSystemReduceMotionForMedia
|
||||
}
|
||||
|
@ -167,6 +176,16 @@ public extension AppPreferences {
|
|||
get { self[.requireDoubleTapToFavorite] ?? false }
|
||||
set { self[.requireDoubleTapToFavorite] = newValue }
|
||||
}
|
||||
|
||||
var notificationPictures: Bool {
|
||||
get { self[.notificationPictures] ?? true }
|
||||
set { self[.notificationPictures] = newValue }
|
||||
}
|
||||
|
||||
var notificationAccountName: Bool {
|
||||
get { self[.notificationAccountName] ?? false }
|
||||
set { self[.notificationAccountName] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppPreferences {
|
||||
|
@ -183,6 +202,9 @@ private extension AppPreferences {
|
|||
case notificationsTabBehavior
|
||||
case defaultEmojiSkinTone
|
||||
case showReblogAndFavoriteCounts
|
||||
case notificationPictures
|
||||
case notificationAccountName
|
||||
case notificationSounds
|
||||
}
|
||||
|
||||
subscript<T>(index: Item) -> T? {
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import Mastodon
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
struct NotificationPreferencesView: View {
|
||||
@StateObject var viewModel: PreferencesViewModel
|
||||
@StateObject var identityContext: IdentityContext
|
||||
|
||||
init(viewModel: PreferencesViewModel) {
|
||||
_viewModel = StateObject(wrappedValue: viewModel)
|
||||
_identityContext = StateObject(wrappedValue: viewModel.identityContext)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Toggle("preferences.notifications.include-pictures",
|
||||
isOn: $identityContext.appPreferences.notificationPictures)
|
||||
Toggle("preferences.notifications.include-account-name",
|
||||
isOn: $identityContext.appPreferences.notificationAccountName)
|
||||
}
|
||||
Section(header: Text("preferences.notifications.sounds")) {
|
||||
ForEach(MastodonNotification.NotificationType.allCasesExceptUnknown) { type in
|
||||
Toggle(type.localizedStringKey, isOn: .init {
|
||||
viewModel.identityContext.appPreferences.notificationSounds.contains(type)
|
||||
} set: {
|
||||
if $0 {
|
||||
viewModel.identityContext.appPreferences.notificationSounds.insert(type)
|
||||
} else {
|
||||
viewModel.identityContext.appPreferences.notificationSounds.remove(type)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("preferences.notifications")
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonNotification.NotificationType {
|
||||
var localizedStringKey: LocalizedStringKey {
|
||||
switch self {
|
||||
case .follow:
|
||||
return "preferences.notification-types.follow"
|
||||
case .mention:
|
||||
return "preferences.notification-types.mention"
|
||||
case .reblog:
|
||||
return "preferences.notification-types.reblog"
|
||||
case .favourite:
|
||||
return "preferences.notification-types.favourite"
|
||||
case .poll:
|
||||
return "preferences.notification-types.poll"
|
||||
case .followRequest:
|
||||
return "preferences.notification-types.follow-request"
|
||||
case .status:
|
||||
return "preferences.notification-types.status"
|
||||
case .unknown:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import PreviewViewModels
|
||||
|
||||
struct NotificationPreferencesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NotificationPreferencesView(viewModel: .init(identityContext: .preview))
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -63,6 +63,8 @@ struct PreferencesView: View {
|
|||
&& viewModel.identityContext.identity.authenticated)
|
||||
}
|
||||
Section(header: Text("preferences.app")) {
|
||||
NavigationLink("preferences.notifications",
|
||||
destination: NotificationPreferencesView(viewModel: viewModel))
|
||||
Picker("preferences.status-word",
|
||||
selection: $identityContext.appPreferences.statusWord) {
|
||||
ForEach(AppPreferences.StatusWord.allCases) { option in
|
||||
|
|
Loading…
Reference in New Issue