diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index dfcfca3..cf534a0 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -163,6 +163,10 @@ "metatext" = "Metatext"; "notification.signed-in-as-%@" = "Logged in as %@"; "notification.new-items" = "New notifications"; +"notification.poll" = "A poll you have voted in has ended"; +"notification.poll.own" = "Your poll has ended"; +"notification.poll.unknown" = "A poll has ended"; +"notification.status-%@" = "%@ just posted"; "notifications.all" = "All"; "notifications.mentions" = "Mentions"; "ok" = "OK"; diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index ee64a5c..b3bb2ca 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -129,6 +129,7 @@ D09D972225C65682007E6394 /* SeparatorConfiguredCollectionViewListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D972125C65682007E6394 /* SeparatorConfiguredCollectionViewListCell.swift */; }; D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; }; D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; }; + D0B325EB25E88ADC00C24BEA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0C7D45724F76169001EBDBB /* Localizable.strings */; }; D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; }; D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; }; D0B8510C25259E56004E0744 /* LoadMoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B8510B25259E56004E0744 /* LoadMoreTableViewCell.swift */; }; @@ -998,6 +999,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D0B325EB25E88ADC00C24BEA /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Notification Service Extension/NotificationService.swift b/Notification Service Extension/NotificationService.swift index 00dc3fe..451c48d 100644 --- a/Notification Service Extension/NotificationService.swift +++ b/Notification Service Extension/NotificationService.swift @@ -1,5 +1,6 @@ // Copyright © 2020 Metabolist. All rights reserved. +import Combine import Mastodon import SDWebImage import ServiceLayer @@ -14,6 +15,7 @@ final class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? + var cancellables = Set() override func didReceive( _ request: UNNotificationRequest, @@ -47,18 +49,27 @@ final class NotificationService: UNNotificationServiceExtension { bestAttemptContent.sound = .default } - if appPreferences.notificationAccountName, - let accountName = try? AllIdentitiesService(environment: Self.environment).identity(id: identityId)?.handle { - bestAttemptContent.subtitle = accountName + var identity: Identity? + + if appPreferences.notificationAccountName { + identity = try? AllIdentitiesService(environment: Self.environment).identity(id: identityId) + + if let handle = identity?.handle { + bestAttemptContent.subtitle = handle + } } - if appPreferences.notificationPictures { - Self.addImage(url: pushNotification.icon, - bestAttemptContent: bestAttemptContent, - contentHandler: contentHandler) - } else { - contentHandler(bestAttemptContent) - } + Self.attachment(imageURL: pushNotification.icon) + .map { [$0] } + .replaceError(with: []) + .handleEvents(receiveOutput: { bestAttemptContent.attachments = $0 }) + .zip(parsingService.title(pushNotification: pushNotification, + identityId: identityId, + accountId: identity?.account?.id) + .replaceError(with: pushNotification.title) + .handleEvents(receiveOutput: { bestAttemptContent.title = $0 })) + .sink { _ in contentHandler(bestAttemptContent) } + .store(in: &cancellables) } override func serviceExtensionTimeWillExpire() { @@ -73,26 +84,32 @@ private extension NotificationService { userNotificationCenter: .current(), reduceMotion: { false }) - static func addImage(url: URL, - bestAttemptContent: UNMutableNotificationContent, - contentHandler: @escaping (UNNotificationContent) -> Void) { - let fileName = url.lastPathComponent + enum ImageError: Error { + case dataMissing + } + + static func attachment(imageURL: URL) -> AnyPublisher { + let fileName = imageURL.lastPathComponent let fileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent(fileName) - SDWebImageManager.shared.loadImage(with: url, options: [], progress: nil) { _, data, _, _, _, _ in - if let data = data { - do { - try data.write(to: fileURL) - bestAttemptContent.attachments = - [try UNNotificationAttachment(identifier: fileName, url: fileURL)] - contentHandler(bestAttemptContent) - } catch { - contentHandler(bestAttemptContent) + return Future { promise in + SDWebImageManager.shared.loadImage(with: imageURL, options: [], progress: nil) { _, data, error, _, _, _ in + if let error = error { + promise(.failure(error)) + } else if let data = data { + let result = Result { + try data.write(to: fileURL) + + return try UNNotificationAttachment(identifier: fileName, url: fileURL) + } + + promise(result) + } else { + promise(.failure(ImageError.dataMissing)) } - } else { - contentHandler(bestAttemptContent) } } + .eraseToAnyPublisher() } } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/PushNotificationParsingService.swift b/ServiceLayer/Sources/ServiceLayer/Services/PushNotificationParsingService.swift index 12beadc..cf72af5 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/PushNotificationParsingService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/PushNotificationParsingService.swift @@ -1,8 +1,10 @@ // Copyright © 2021 Metabolist. All rights reserved. +import Combine import CryptoKit import Foundation import Mastodon +import MastodonAPI import Secrets enum NotificationExtensionServiceError: Error { @@ -48,6 +50,53 @@ public extension PushNotificationParsingService { salt: salt), identityId) } + + func title(pushNotification: PushNotification, + identityId: Identity.Id, + accountId: Account.Id?) -> AnyPublisher { + switch pushNotification.notificationType { + case .poll, .status: + let secrets = Secrets(identityId: identityId, keychain: environment.keychain) + let instanceURL: URL + + do { + instanceURL = try secrets.getInstanceURL() + } catch { + return Fail(error: error).eraseToAnyPublisher() + } + + let mastodonAPIClient = MastodonAPIClient(session: .shared, instanceURL: instanceURL) + + mastodonAPIClient.accessToken = pushNotification.accessToken + + let endpoint = NotificationEndpoint.notification(id: String(pushNotification.notificationId)) + + return mastodonAPIClient.request(endpoint) + .map { + switch pushNotification.notificationType { + case .status: + return String.localizedStringWithFormat( + NSLocalizedString("notification.status-%@", comment: ""), + $0.account.displayName) + case .poll: + switch accountId ?? (try? AllIdentitiesService(environment: environment) + .identity(id: identityId)?.account?.id) { + case .some($0.account.id): + return NSLocalizedString("notification.poll.own", comment: "") + case .some: + return NSLocalizedString("notification.poll", comment: "") + default: + return NSLocalizedString("notification.poll.unknown", comment: "") + } + default: + return pushNotification.title + } + } + .eraseToAnyPublisher() + default: + return Just(pushNotification.title).setFailureType(to: Error.self).eraseToAnyPublisher() + } + } } private extension PushNotificationParsingService {