mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-13 03:09:58 +02:00
c64993ad1d
* Update voice broadcast tiles UI (#6965) * Translated using Weblate (German) Currently translated at 100.0% (2307 of 2307 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * speeding the animation a bit * tests and identifier improvements * fix * changelog * removed unused code * Avoid unnecessary send state request (#6970) * comment * Curate MXCrypto protocol methods * Add voice broadcast initial state in bubble data (#6972) - Add voice broadcast initial state in bubble data - Remove the local record after sending * Voice Broadcast: log and block unexpected state change * Sing out bottom bar * new line * Enable WYSIWYG plain text support * Remove change on Apple swift-collections revision * removed RiotSettings a non RiotSwiftUI reference from the ViewState code * fixed a test * Complete MXCryptoV2 implementation * Multi session logut * Switch the CI to code 14 and the iOS 14 simulator, fix UI tests * Fixes #6987 - Prevent ZXing from unnecessarily requesting camera access * Fixes #6988 - Prevent actor switching when tearing down the rendezvous * Separator fix * Removed warnings * add Z-Labs tag or rich text editor and update to the new label naming * changelog * Hide old sessions list when the new dm is enabled * Add changelog.d file * Sing out filtering * Avoid simultaneous state changes (#6986) * Improve kebab menu in UserSessionOverview * Add UI tests * Add changelog.d file * No customization for emptycell (#7000) * PSG-976 Exclude current session from security recommendations and other sessions * Padding fix * Fixed unit tests * Add empty onLearnMoreAction closure * Add InfoView skeleton * Add UserSessionOverviewViewBindings * Style info view * Add bottom sheet modifier * Localise content * Add inactive sessions copy * Fix bug in InlineTextButton * Improve UserSessionCardView * Add “learn more” button in UserOtherSessions * Show bottom sheet in user other sessions * Show rename info alert * Refine UX * Add iOS 15- fallback * Refine InfoView * Add UI tests * Improve UserOtherSessionsUITests * Improve InlineTextButton API * Add changelod.d file * Fix failing UTs * Hide keyboard in UserSessionName * Add .viewSessionInfo view action * Voice Broadcast - BugFix - send the last chunk (#7002) * Voice Broadcast - BugFix - send the last chunk with the right sequence number - we reset now and teardown the service only after the last chunk is sent * updated package + tests * change log * Bug Fix : Crash if the room has avatar and voice broadcast tiles * Add MVVM-C for InfoSheet * improving UI tests for slow CI * removing comment * test improvements for slow ci * Show bottom sheet in other sessions screen * Show bottom sheet in rename session screen * Delete bottom sheet modifier * Show rename sheet * UI and unit tests * Refresh fix * Changelog * Add InfoSheet SwiftUI preview * simplify the test to make it pass on the CI * Fix memory leak * Cleanup UI tests * improving tests for the CI * Fixed IRC-style message and commands support in Rich text editor * tests updated for the CI * test improvements * removing a test that can't pass on the CI due to its speed * Changelog * CryptoV2 changes * Display crypto version * Voice broadcast - Disable the sleep mode during the recording until we are able to handle it Currently go to "sleep mode" pauses the voice broadcast recording * Add issue automation for the VoIP team * Renamed sign out to logout * Renamed sign out to logout * Renamed sign out to logout * Sign out of all other sessions * Fix typo in issue automation * Fixed unit tests * Translations update from Weblate (#7017) * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (German) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (German) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com> Co-authored-by: Priit Jõerüüt <riot@joeruut.com> Co-authored-by: Vri <element@vrifox.cc> Co-authored-by: Johan Smits <johan@smitsmail.net> * Prepare for new sprint * Prepare for new sprint * Threads: added support to read receipts (MSC3771) - Update after review * Threads: added support to notifications count (MSC3773) * Update RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com> * Update RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com> * Comment fix * the test may fail on CI without blocking the task/check * tests may fail on CI * test improvement * test may fail on CI * Hide push toggles for http pushers when there is no server support * changelog * Code review fixes * Threads: added support to read receipts (MSC3771) - Update after review * Synchronise composer and toolbar resizing animation duration * Add kResizeComposerAnimationDuration constant description * fix for 6946 * Threads: add support to labs flag for read receipts * Cleanup * Code review fixes, created DestructiveButton * Update issue automation Stop using deprecated ProjectNext API in favour of the new ProjectV2 one * Update PR automation Stop using deprecated ProjectNext API in favour of the new ProjectV2 one * Code review fixes * Map location info * Map location info * Add location feature in UserSessionsOverview * Add “show location” feature in other sessions list * Add “show location“ feature in session overview * Fix Package.resolved * Cleanup merge leftovers * Cleanup code * Cleanup * Add show/hide ip persistency * Add location info in UserOtherSessions * Refine settings logic * Mock settings in UserSessionsOverviewViewModel * Add settings service in UserOtherSessionsViewModel * Inject setting service in UserSessionOverviewViewModel * Add changelog.d file * Fix UTs * Cleanup merge leftovers * Add animations * Fix failing test * Amend title font * Amend copies * Device Manager: Session list item is not tappable everywhere * changelog * Threads notification count in main timeline including un participated threads * Changed title and body * Removed "Do not ask again" button * Remove indication about plain text mode coming soon * Prevent `Unable to activate constraint with anchors .. because they have no common ancestor.` crashes. Only link toasts to the top safe area instead of the navigation controller * Revert "Replace attributed string height calculation with a more reliable implementation" This reverts commit47512127ec. * Revert "Fix timeline items text height calculation" This reverts commit27f4feb8d9. * Revert "Fixes vector-im/element-ios/issues/6441 - Incorrect timeline item text height calculation (#6679)" This reverts commitbd017d1a77. * Fixes vector-im/element-ios/issues/6441 - Incorrect timeline item text height calculation * Prepare for new sprint * Refine bottom sheet layout * updated pod * changelog * Switch to using an API key for interactions with AppStoreConnect while on CI; update fastlane and dependencies * Rich-text editor: Fix text formatting enabled inconsistent state * Labs: Rich-text editor - Fix text formatting switch losing the current content of the composer * Re-order View computed properties and move to private mark * Add intrinsic sized bottom sheet * Snooze controller * Changelog * Fix composer view model tests * Rich-text editor: enable translations between Markdown and HTML when toggling text formatting * Force a layout on the room bubble cell messageTextView to get a correct frame * Move Move UserAgentParserTests * Add UserSessionDetailsUITests * Improve UserSessionNameUITests * Cleanup tests * Improve UserSessionNameViewModelTests * Test empty state for UserOtherSessions * Fix typo * Cleanup unused code * Add changelog.d file * Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773 * Remove 10s wait on failed initial sync * Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773 - Update after review * Revert "Device Manager: Session list item is not tappable everywhere" This reverts commit221f4ad106. * Fixup session list item is not tappable everywhere * Fix accessibility id in UserOtherSessions * Poll not usable after logging out and back in * Changelog * Removed init * voice dictation now works * plain text * Add voice broadcast slider (#7010) * Display number of unread messages above threads button * Translations update from Weblate (#7080) * Translated using Weblate (Dutch) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Albanian) Currently translated at 99.6% (2303 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (German) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ * Translated using Weblate (Italian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Albanian) Currently translated at 99.5% (2305 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ * Translated using Weblate (German) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Japanese) Currently translated at 66.2% (1534 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Japanese) Currently translated at 66.2% (1534 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ * Translated using Weblate (German) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (German) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (French) Currently translated at 97.6% (2272 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ * Translated using Weblate (Russian) Currently translated at 80.5% (1873 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ * Translated using Weblate (Russian) Currently translated at 80.7% (1879 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ Co-authored-by: Roel ter Maat <roel.termaat@nedap.com> Co-authored-by: Vri <element@vrifox.cc> Co-authored-by: Besnik Bleta <besnik@programeshqip.org> Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com> Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com> Co-authored-by: Priit Jõerüüt <riot@joeruut.com> Co-authored-by: Linerly <linerly@protonmail.com> Co-authored-by: Jozef Gaal <preklady@mayday.sk> Co-authored-by: random <dictionary@tutamail.com> Co-authored-by: Szimszon <github@oregpreshaz.eu> Co-authored-by: Suguru Hirahara <ovestekona@protonmail.com> Co-authored-by: Thibault Martin <mail@thibaultmart.in> Co-authored-by: Platon Terekhov <ockenfels_vevent@aleeas.com> * changelog.d: Upgrade MatrixSDK version ([v0.24.3](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.3)). * version++ Co-authored-by: giomfo <gforet@matrix.org> Co-authored-by: Yoan Pintas <y.pintas@gmail.com> Co-authored-by: Vri <element@vrifox.cc> Co-authored-by: Anderas <andyuhnak@gmail.com> Co-authored-by: Mauro Romito <mauro.romito@element.io> Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> Co-authored-by: Giom Foret <giom@matrix.org> Co-authored-by: Aleksandrs Proskurins <paleksandrs@gmail.com> Co-authored-by: aringenbach <arnaudr@element.io> Co-authored-by: manuroe <manuroe@users.noreply.github.com> Co-authored-by: David Langley <langley.dave@gmail.com> Co-authored-by: Stefan Ceriu <stefanc@matrix.org> Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com> Co-authored-by: Alfonso Grillo <alfogrillo@element.io> Co-authored-by: Kat Gerasimova <ekaterinag@element.io> Co-authored-by: Element Translate Bot <admin@riot.im> Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com> Co-authored-by: Priit Jõerüüt <riot@joeruut.com> Co-authored-by: Johan Smits <johan@smitsmail.net> Co-authored-by: gulekismail <ismailgulek0@gmail.com> Co-authored-by: Gil Eluard <gile@element.io> Co-authored-by: Aleksandrs Proskurins <aleksandrsp@element.io> Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com> Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com> Co-authored-by: Roel ter Maat <roel.termaat@nedap.com> Co-authored-by: Besnik Bleta <besnik@programeshqip.org> Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com> Co-authored-by: Linerly <linerly@protonmail.com> Co-authored-by: Jozef Gaal <preklady@mayday.sk> Co-authored-by: random <dictionary@tutamail.com> Co-authored-by: Szimszon <github@oregpreshaz.eu> Co-authored-by: Suguru Hirahara <ovestekona@protonmail.com> Co-authored-by: Thibault Martin <mail@thibaultmart.in> Co-authored-by: Platon Terekhov <ockenfels_vevent@aleeas.com>
866 lines
46 KiB
Swift
866 lines
46 KiB
Swift
/*
|
|
Copyright 2020 New Vector Ltd
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
import UserNotifications
|
|
import MatrixSDK
|
|
|
|
/// The number of milliseconds in one second.
|
|
private let MSEC_PER_SEC: TimeInterval = 1000
|
|
|
|
class NotificationService: UNNotificationServiceExtension {
|
|
|
|
private struct NSE {
|
|
enum Constants {
|
|
static let voipPushRequestTimeout: TimeInterval = 15
|
|
static let timeNeededToSendVoIPPushes: TimeInterval = 20
|
|
}
|
|
}
|
|
|
|
// MARK: - Properties
|
|
|
|
/// Receiving dates for notifications. Keys are eventId's
|
|
private var receiveDates: [String: Date] = [:]
|
|
|
|
/// Content handlers. Keys are eventId's
|
|
private var contentHandlers: [String: ((UNNotificationContent) -> Void)] = [:]
|
|
|
|
/// Flags to indicate there is an ongoing VoIP Push request for events. Keys are eventId's
|
|
private var ongoingVoIPPushRequests: [String: Bool] = [:]
|
|
|
|
private var userAccount: MXKAccount?
|
|
|
|
/// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's
|
|
private var bestAttemptContents: [String: UNMutableNotificationContent] = [:]
|
|
|
|
private static var backgroundSyncService: MXBackgroundSyncService!
|
|
private var showDecryptedContentInNotifications: Bool {
|
|
return RiotSettings.shared.showDecryptedContentInNotifications
|
|
}
|
|
private lazy var configuration: Configurable = {
|
|
return CommonConfiguration()
|
|
}()
|
|
private lazy var mxRestClient: MXRestClient? = {
|
|
guard let userAccount = userAccount else {
|
|
return nil
|
|
}
|
|
let restClient = MXRestClient(credentials: userAccount.mxCredentials, unrecognizedCertificateHandler: nil, persistentTokenDataHandler: { persistTokenDataHandler in
|
|
MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler)
|
|
}, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in
|
|
userAccount.handleUnauthenticatedWithError(error, isSoftLogout: softLogout, isRefreshTokenAuth: refreshTokenAuth, andCompletion: completion)
|
|
})
|
|
return restClient
|
|
}()
|
|
|
|
private static var isLoggerInitialized: Bool = false
|
|
private lazy var pushGatewayRestClient: MXPushGatewayRestClient = {
|
|
let url = URL(string: BuildSettings.serverConfigSygnalAPIUrlString)!
|
|
return MXPushGatewayRestClient(pushGateway: url.scheme! + "://" + url.host!, andOnUnrecognizedCertificateBlock: nil)
|
|
}()
|
|
private var pushNotificationStore: PushNotificationStore = PushNotificationStore()
|
|
private let localAuthenticationService = LocalAuthenticationService(pinCodePreferences: .shared)
|
|
private static let backgroundServiceInitQueue = DispatchQueue(label: "io.element.NotificationService.backgroundServiceInitQueue")
|
|
// MARK: - Method Overrides
|
|
|
|
override init() {
|
|
super.init()
|
|
|
|
// Set up runtime language and fallback by considering the userDefaults object shared within the application group.
|
|
let sharedUserDefaults = MXKAppSettings.standard().sharedUserDefaults
|
|
if let language = sharedUserDefaults?.string(forKey: "appLanguage") {
|
|
Bundle.mxk_setLanguage(language)
|
|
}
|
|
Bundle.mxk_setFallbackLanguage("en")
|
|
}
|
|
|
|
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
|
let userInfo = request.content.userInfo
|
|
|
|
// Set static application settings
|
|
configuration.setupSettings()
|
|
|
|
if DataProtectionHelper.isDeviceInRebootedAndLockedState(appGroupIdentifier: MXSDKOptions.sharedInstance().applicationGroupIdentifier) {
|
|
// kill the process in this state, this leads for the notification to be displayed as came from APNS
|
|
exit(0)
|
|
}
|
|
|
|
// setup logs
|
|
setupLogger()
|
|
|
|
MXLog.debug(" ")
|
|
MXLog.debug(" ")
|
|
MXLog.debug("################################################################################")
|
|
MXLog.debug("[NotificationService] Instance: \(self), thread: \(Thread.current)")
|
|
MXLog.debug("[NotificationService] Payload came: \(userInfo)")
|
|
|
|
// log memory at the beginning of the process
|
|
logMemory()
|
|
|
|
setupAnalytics()
|
|
|
|
UNUserNotificationCenter.current().removeUnwantedNotifications()
|
|
|
|
// check if this is a Matrix notification
|
|
guard let roomId = userInfo["room_id"] as? String, let eventId = userInfo["event_id"] as? String else {
|
|
// it's not a Matrix notification, do not change the content
|
|
MXLog.debug("[NotificationService] didReceiveRequest: This is not a Matrix notification.")
|
|
contentHandler(request.content)
|
|
return
|
|
}
|
|
|
|
// save this content as fallback content
|
|
guard let content = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
|
return
|
|
}
|
|
|
|
// store receive date
|
|
receiveDates[eventId] = Date()
|
|
|
|
// read badge from "unread_count"
|
|
// no need to check before, if it's nil, the badge will remain unchanged
|
|
content.badge = userInfo["unread_count"] as? NSNumber
|
|
|
|
bestAttemptContents[eventId] = content
|
|
contentHandlers[eventId] = contentHandler
|
|
|
|
// setup user account
|
|
setup(withRoomId: roomId, eventId: eventId) {
|
|
// preprocess the payload, will attempt to fetch room display name
|
|
self.preprocessPayload(forEventId: eventId, roomId: roomId)
|
|
// fetch the event first
|
|
self.fetchAndProcessEvent(withEventId: eventId, roomId: roomId)
|
|
}
|
|
}
|
|
|
|
override func serviceExtensionTimeWillExpire() {
|
|
// Called just before the extension will be terminated by the system.
|
|
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
|
|
|
MXLog.debug("[NotificationService] serviceExtensionTimeWillExpire")
|
|
// No-op here. If the process is killed by the OS due to time limit, it will also show the notification with the original content.
|
|
}
|
|
|
|
deinit {
|
|
MXLog.debug("[NotificationService] deinit for \(self)");
|
|
self.logMemory()
|
|
MXLog.debug(" ")
|
|
}
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
private func logMemory() {
|
|
MXLog.debug("[NotificationService] Memory: footprint: \(MXMemory.formattedMemoryFootprint()) - available: \(MXMemory.formattedMemoryAvailable())")
|
|
}
|
|
|
|
private func setupLogger() {
|
|
if !NotificationService.isLoggerInitialized {
|
|
let configuration = MXLogConfiguration()
|
|
configuration.logLevel = .verbose
|
|
configuration.maxLogFilesCount = 100
|
|
configuration.logFilesSizeLimit = 10 * 1024 * 1024; // 10MB
|
|
configuration.subLogName = "nse"
|
|
|
|
if isatty(STDERR_FILENO) == 0 {
|
|
configuration.redirectLogsToFiles = true
|
|
}
|
|
|
|
MXLog.configure(configuration)
|
|
|
|
NotificationService.isLoggerInitialized = true
|
|
}
|
|
}
|
|
|
|
private func setupAnalytics(){
|
|
// Configure our analytics. It will start if the option is enabled
|
|
let analytics = Analytics.shared
|
|
MXSDKOptions.sharedInstance().analyticsDelegate = analytics
|
|
analytics.startIfEnabled()
|
|
}
|
|
|
|
private func setup(withRoomId roomId: String, eventId: String, completion: @escaping () -> Void) {
|
|
MXKAccountManager.sharedManager(withReload: true)
|
|
self.userAccount = MXKAccountManager.shared()?.activeAccounts.first
|
|
if let userAccount = userAccount {
|
|
Self.backgroundServiceInitQueue.sync {
|
|
if NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials {
|
|
MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE")
|
|
self.logMemory()
|
|
NotificationService.backgroundSyncService = MXBackgroundSyncService(withCredentials: userAccount.mxCredentials, persistTokenDataHandler: { persistTokenDataHandler in
|
|
MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler)
|
|
}, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in
|
|
userAccount.handleUnauthenticatedWithError(error, isSoftLogout: softLogout, isRefreshTokenAuth: refreshTokenAuth, andCompletion: completion)
|
|
})
|
|
MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: AFTER")
|
|
self.logMemory()
|
|
}
|
|
completion()
|
|
}
|
|
} else {
|
|
MXLog.debug("[NotificationService] setup: No active accounts")
|
|
fallbackToBestAttemptContent(forEventId: eventId)
|
|
}
|
|
}
|
|
|
|
/// Attempts to preprocess payload and attach room display name to the best attempt content
|
|
/// - Parameters:
|
|
/// - eventId: Event identifier to mutate best attempt content
|
|
/// - roomId: Room identifier to fetch display name
|
|
private func preprocessPayload(forEventId eventId: String, roomId: String) {
|
|
if localAuthenticationService.isProtectionSet {
|
|
MXLog.debug("[NotificationService] preprocessPayload: Do not preprocess because app protection is set")
|
|
return
|
|
}
|
|
|
|
// If a room summary is available, use the displayname for the best attempt title.
|
|
guard let roomSummary = NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId) else { return }
|
|
guard let roomDisplayName = roomSummary.displayname else { return }
|
|
bestAttemptContents[eventId]?.title = roomDisplayName
|
|
|
|
// At this stage we don't know the message type, so leave the body as set in didReceive.
|
|
}
|
|
|
|
private func fetchAndProcessEvent(withEventId eventId: String, roomId: String) {
|
|
MXLog.debug("[NotificationService] fetchAndProcessEvent")
|
|
|
|
NotificationService.backgroundSyncService.event(withEventId: eventId, inRoom: roomId) { [weak self] (response) in
|
|
switch response {
|
|
case .success(let event):
|
|
MXLog.debug("[NotificationService] fetchAndProcessEvent: Event fetched successfully")
|
|
self?.checkPlaybackAndContinueProcessing(event, roomId: roomId)
|
|
case .failure(let error):
|
|
MXLog.error("[NotificationService] fetchAndProcessEvent: Failed fetching notification event", context: error)
|
|
self?.fallbackToBestAttemptContent(forEventId: eventId)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func checkPlaybackAndContinueProcessing(_ notificationEvent: MXEvent, roomId: String) {
|
|
NotificationService.backgroundSyncService.readMarkerEvent(forRoomId: roomId) { [weak self] response in
|
|
switch response {
|
|
case .success(let readMarkerEvent):
|
|
MXLog.debug("[NotificationService] checkPlaybackAndContinueProcessing: Read marker event fetched successfully")
|
|
|
|
// As origin server timestamps are not always correct data in a federated environment, we add 10 minutes
|
|
// to the calculation to reduce the possibility that an event is marked as read which isn't.
|
|
let notificationTimestamp = notificationEvent.originServerTs + (10 * 60 * 1000)
|
|
|
|
if readMarkerEvent.originServerTs > notificationTimestamp {
|
|
MXLog.error("[NotificationService] checkPlaybackAndContinueProcessing: Event already read, discarding.")
|
|
self?.discardEvent(event: notificationEvent)
|
|
} else {
|
|
self?.processEvent(notificationEvent)
|
|
}
|
|
|
|
case .failure(let error):
|
|
MXLog.error("[NotificationService] checkPlaybackAndContinueProcessing: Failed fetching read marker event", context: error)
|
|
self?.processEvent(notificationEvent)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func processEvent(_ event: MXEvent) {
|
|
if let receiveDate = receiveDates[event.eventId] {
|
|
MXLog.debug("[NotificationService] processEvent: notification receive delay: \(receiveDate.timeIntervalSince1970*MSEC_PER_SEC - TimeInterval(event.originServerTs)) ms")
|
|
}
|
|
|
|
guard let content = bestAttemptContents[event.eventId], let userAccount = userAccount else {
|
|
self.fallbackToBestAttemptContent(forEventId: event.eventId)
|
|
return
|
|
}
|
|
|
|
self.notificationContent(forEvent: event, forAccount: userAccount) { [weak self] (notificationContent, ignoreBadgeUpdate) in
|
|
guard let self = self else { return }
|
|
|
|
guard let newContent = notificationContent else {
|
|
// We still want them removed if the NSE filtering entitlement is not available
|
|
content.categoryIdentifier = Constants.toBeRemovedNotificationCategoryIdentifier
|
|
self.discardEvent(event: event)
|
|
return
|
|
}
|
|
|
|
content.title = newContent.title
|
|
content.subtitle = newContent.subtitle
|
|
content.body = newContent.body
|
|
content.threadIdentifier = newContent.threadIdentifier
|
|
content.categoryIdentifier = newContent.categoryIdentifier
|
|
content.userInfo = newContent.userInfo
|
|
content.sound = newContent.sound
|
|
|
|
if ignoreBadgeUpdate {
|
|
content.badge = nil
|
|
}
|
|
|
|
if self.ongoingVoIPPushRequests[event.eventId] == true {
|
|
// modify the best attempt content, to be able to use in the future
|
|
self.bestAttemptContents[event.eventId] = content
|
|
|
|
// There is an ongoing VoIP Push request for this event, wait for it to be completed.
|
|
// When it completes, it'll continue with the bestAttemptContent.
|
|
return
|
|
} else {
|
|
self.finishProcessing(forEventId: event.eventId, withContent: content)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func discardEvent(event:MXEvent) {
|
|
MXLog.debug("[NotificationService] discardEvent: Discarding event: \(String(describing: event.eventId))")
|
|
finishProcessing(forEventId: event.eventId, withContent: UNNotificationContent())
|
|
}
|
|
|
|
private func fallbackToBestAttemptContent(forEventId eventId: String) {
|
|
MXLog.debug("[NotificationService] fallbackToBestAttemptContent: method called.")
|
|
|
|
guard let content = bestAttemptContents[eventId] else {
|
|
MXLog.debug("[NotificationService] fallbackToBestAttemptContent: Best attempt content is missing.")
|
|
return
|
|
}
|
|
|
|
finishProcessing(forEventId: eventId, withContent: content)
|
|
}
|
|
|
|
private func finishProcessing(forEventId eventId: String, withContent content: UNNotificationContent) {
|
|
MXLog.debug("[NotificationService] finishProcessingEvent: Calling content handler for: \(String(describing: eventId))")
|
|
|
|
contentHandlers[eventId]?(content)
|
|
|
|
// clear maps
|
|
contentHandlers.removeValue(forKey: eventId)
|
|
bestAttemptContents.removeValue(forKey: eventId)
|
|
receiveDates.removeValue(forKey: eventId)
|
|
|
|
// We are done for this push
|
|
MXLog.debug("--------------------------------------------------------------------------------")
|
|
}
|
|
|
|
private func notificationContent(forEvent event: MXEvent, forAccount account: MXKAccount, onComplete: @escaping (UNNotificationContent?, Bool) -> Void) {
|
|
guard let content = event.content, content.count > 0 else {
|
|
MXLog.debug("[NotificationService] notificationContentForEvent: empty event content")
|
|
onComplete(nil, false)
|
|
return
|
|
}
|
|
|
|
let roomId = event.roomId!
|
|
let isRoomMentionsOnly = NotificationService.backgroundSyncService.isRoomMentionsOnly(roomId)
|
|
let roomSummary = NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId)
|
|
|
|
MXLog.debug("[NotificationService] notificationContentForEvent: Attempt to fetch the room state")
|
|
|
|
self.context(ofEvent: event, inRoom: roomId, completion: { (response) in
|
|
switch response {
|
|
case .success(let (roomState, eventSenderName)):
|
|
var notificationTitle: String?
|
|
var notificationBody: String?
|
|
var additionalUserInfo: [AnyHashable: Any]?
|
|
var ignoreBadgeUpdate = false
|
|
var threadIdentifier: String? = roomId
|
|
let currentUserId = account.mxCredentials.userId
|
|
let roomDisplayName = roomSummary?.displayname
|
|
let pushRule = NotificationService.backgroundSyncService.pushRule(matching: event, roomState: roomState)
|
|
|
|
switch event.eventType {
|
|
case .callInvite:
|
|
let offer = event.content["offer"] as? [AnyHashable: Any]
|
|
let sdp = offer?["sdp"] as? String
|
|
let isVideoCall = sdp?.contains("m=video") ?? false
|
|
|
|
if isVideoCall {
|
|
notificationBody = NotificationService.localizedString(forKey: "VIDEO_CALL_FROM_USER", eventSenderName)
|
|
} else {
|
|
notificationBody = NotificationService.localizedString(forKey: "VOICE_CALL_FROM_USER", eventSenderName)
|
|
}
|
|
|
|
// call notifications should stand out from normal messages, so we don't stack them
|
|
threadIdentifier = nil
|
|
|
|
if let callInviteContent = MXCallInviteEventContent(fromJSON: event.content),
|
|
callInviteContent.lifetime > event.age,
|
|
(callInviteContent.lifetime - event.age) > UInt(NSE.Constants.timeNeededToSendVoIPPushes * MSEC_PER_SEC) {
|
|
NotificationService.backgroundSyncService.roomAccountData(forRoomId: roomId) { response in
|
|
if let accountData = response.value, accountData.virtualRoomInfo.isVirtual {
|
|
self.sendReadReceipt(forEvent: event)
|
|
ignoreBadgeUpdate = true
|
|
}
|
|
self.validateNotificationContentAndComplete(
|
|
notificationTitle: notificationTitle,
|
|
notificationBody: notificationBody,
|
|
additionalUserInfo: additionalUserInfo,
|
|
ignoreBadgeUpdate: ignoreBadgeUpdate,
|
|
threadIdentifier: threadIdentifier,
|
|
currentUserId: currentUserId,
|
|
event: event,
|
|
pushRule: pushRule,
|
|
onComplete: onComplete
|
|
)
|
|
}
|
|
self.sendVoipPush(forEvent: event)
|
|
return
|
|
} else {
|
|
MXLog.debug("[NotificationService] notificationContent: Do not attempt to send a VoIP push, there is not enough time to process it.")
|
|
}
|
|
case .roomEncrypted:
|
|
// If unable to decrypt the event, use the fallback.
|
|
break
|
|
case .roomMessage:
|
|
if isRoomMentionsOnly {
|
|
// A local notification will be displayed only for highlighted notification.
|
|
var isHighlighted = false
|
|
|
|
// Check whether is there an highlight tweak on it
|
|
for ruleAction in pushRule?.actions ?? [] {
|
|
guard let action = ruleAction as? MXPushRuleAction else { continue }
|
|
guard action.actionType == MXPushRuleActionTypeSetTweak else { continue }
|
|
guard action.parameters["set_tweak"] as? String == "highlight" else { continue }
|
|
// Check the highlight tweak "value"
|
|
// If not present, highlight. Else check its value before highlighting
|
|
if nil == action.parameters["value"] || true == (action.parameters["value"] as? Bool) {
|
|
isHighlighted = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !isHighlighted {
|
|
// In practice, this only hides the notification's content. An empty notification may be less useful in this instance?
|
|
// Ignore this notif.
|
|
MXLog.debug("[NotificationService] notificationContentForEvent: Ignore non highlighted notif in mentions only room")
|
|
onComplete(nil, false)
|
|
return
|
|
}
|
|
}
|
|
|
|
let msgType = event.content[kMXMessageTypeKey] as? String
|
|
let messageContent = event.content[kMXMessageBodyKey] as? String ?? ""
|
|
let isReply = event.isReply()
|
|
|
|
if isReply {
|
|
notificationTitle = self.replyTitle(for: eventSenderName, in: roomDisplayName)
|
|
} else {
|
|
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
|
|
}
|
|
|
|
if event.isEncrypted && !self.showDecryptedContentInNotifications {
|
|
// Hide the content
|
|
notificationBody = NotificationService.localizedString(forKey: "MESSAGE")
|
|
break
|
|
}
|
|
|
|
if event.location != nil {
|
|
notificationBody = NotificationService.localizedString(forKey: "LOCATION_FROM_USER", eventSenderName)
|
|
break
|
|
}
|
|
|
|
switch msgType {
|
|
case kMXMessageTypeEmote:
|
|
notificationBody = NotificationService.localizedString(forKey: "ACTION_FROM_USER", eventSenderName, messageContent)
|
|
case kMXMessageTypeImage:
|
|
notificationBody = NotificationService.localizedString(forKey: "PICTURE_FROM_USER", eventSenderName)
|
|
case kMXMessageTypeVideo:
|
|
notificationBody = NotificationService.localizedString(forKey: "VIDEO_FROM_USER", eventSenderName)
|
|
case kMXMessageTypeAudio:
|
|
if event.isVoiceMessage() {
|
|
notificationBody = NotificationService.localizedString(forKey: "VOICE_MESSAGE_FROM_USER", eventSenderName)
|
|
} else {
|
|
notificationBody = NotificationService.localizedString(forKey: "AUDIO_FROM_USER", eventSenderName, messageContent)
|
|
}
|
|
case kMXMessageTypeFile:
|
|
notificationBody = NotificationService.localizedString(forKey: "FILE_FROM_USER", eventSenderName, messageContent)
|
|
|
|
// All other message types such as text, notice, server notice etc
|
|
default:
|
|
if event.isReply() {
|
|
let parser = MXReplyEventParser()
|
|
let replyParts = parser.parse(event)
|
|
notificationBody = replyParts?.bodyParts.replyText
|
|
} else {
|
|
notificationBody = messageContent
|
|
}
|
|
}
|
|
case .roomMember:
|
|
// If the current user is already joined, display updated displayname/avatar events.
|
|
// This is an unexpected path, but has been seen in some circumstances.
|
|
if NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId)?.membership == .join {
|
|
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
|
|
|
|
// If the sender's membership is join and hasn't changed.
|
|
if let newContent = MXRoomMemberEventContent(fromJSON: event.content),
|
|
let prevContentDict = event.prevContent,
|
|
let oldContent = MXRoomMemberEventContent(fromJSON: prevContentDict),
|
|
newContent.membership == kMXMembershipStringJoin,
|
|
oldContent.membership == kMXMembershipStringJoin {
|
|
|
|
// Check for display name changes
|
|
if newContent.displayname != oldContent.displayname {
|
|
// If there was a change, use the sender's userID if one was blank and show the change.
|
|
if let oldDisplayname = oldContent.displayname ?? event.sender,
|
|
let displayname = newContent.displayname ?? event.sender {
|
|
notificationBody = NotificationService.localizedString(forKey: "USER_UPDATED_DISPLAYNAME", oldDisplayname, displayname)
|
|
} else {
|
|
// Should never be reached as the event should always have a sender.
|
|
notificationBody = NotificationService.localizedString(forKey: "GENERIC_USER_UPDATED_DISPLAYNAME", eventSenderName)
|
|
}
|
|
} else {
|
|
// If the display name hasn't changed, handle as an avatar change.
|
|
notificationBody = NotificationService.localizedString(forKey: "USER_UPDATED_AVATAR", eventSenderName)
|
|
}
|
|
} else {
|
|
// No known reports of having reached this situation for a membership notification
|
|
// So use a generic membership updated fallback.
|
|
notificationBody = NotificationService.localizedString(forKey: "USER_MEMBERSHIP_UPDATED", eventSenderName)
|
|
}
|
|
// Otherwise treat the notification as an invite.
|
|
// This is the expected notification content for a membership event.
|
|
} else {
|
|
if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName {
|
|
notificationBody = NotificationService.localizedString(forKey: "USER_INVITE_TO_NAMED_ROOM", eventSenderName, roomDisplayName)
|
|
} else {
|
|
notificationBody = NotificationService.localizedString(forKey: "USER_INVITE_TO_CHAT", eventSenderName)
|
|
}
|
|
}
|
|
|
|
case .sticker:
|
|
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
|
|
notificationBody = NotificationService.localizedString(forKey: "STICKER_FROM_USER", eventSenderName)
|
|
|
|
// Reactions are unexpected notification types, but have been seen in some circumstances.
|
|
case .reaction:
|
|
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
|
|
if let reactionKey = event.relatesTo?.key {
|
|
// Try to show the reaction key in the notification.
|
|
notificationBody = NotificationService.localizedString(forKey: "REACTION_FROM_USER", eventSenderName, reactionKey)
|
|
} else {
|
|
// Otherwise show a generic reaction.
|
|
notificationBody = NotificationService.localizedString(forKey: "GENERIC_REACTION_FROM_USER", eventSenderName)
|
|
}
|
|
|
|
case .custom:
|
|
if (event.type == kWidgetMatrixEventTypeString || event.type == kWidgetModularEventTypeString),
|
|
let type = event.content?["type"] as? String,
|
|
(type == kWidgetTypeJitsiV1 || type == kWidgetTypeJitsiV2) {
|
|
notificationBody = NotificationService.localizedString(forKey: "GROUP_CALL_STARTED")
|
|
notificationTitle = roomDisplayName
|
|
|
|
// call notifications should stand out from normal messages, so we don't stack them
|
|
threadIdentifier = nil
|
|
// only send VoIP pushes if ringing is enabled for group calls
|
|
if RiotSettings.shared.enableRingingForGroupCalls {
|
|
self.sendVoipPush(forEvent: event)
|
|
} else {
|
|
additionalUserInfo = [Constants.userInfoKeyPresentNotificationOnForeground: true]
|
|
}
|
|
}
|
|
case .pollStart:
|
|
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
|
|
notificationBody = MXEventContentPollStart(fromJSON: event.content)?.question
|
|
default:
|
|
break
|
|
}
|
|
|
|
|
|
self.validateNotificationContentAndComplete(
|
|
notificationTitle: notificationTitle,
|
|
notificationBody: notificationBody,
|
|
additionalUserInfo: additionalUserInfo,
|
|
ignoreBadgeUpdate: ignoreBadgeUpdate,
|
|
threadIdentifier: threadIdentifier,
|
|
currentUserId: currentUserId,
|
|
event: event,
|
|
pushRule: pushRule,
|
|
onComplete: onComplete
|
|
)
|
|
case .failure(let error):
|
|
MXLog.debug("[NotificationService] notificationContentForEvent: error: \(error)")
|
|
onComplete(nil, false)
|
|
}
|
|
})
|
|
}
|
|
|
|
private func validateNotificationContentAndComplete(
|
|
notificationTitle: String?,
|
|
notificationBody: String?,
|
|
additionalUserInfo: [AnyHashable: Any]?,
|
|
ignoreBadgeUpdate: Bool,
|
|
threadIdentifier: String?,
|
|
currentUserId: String?,
|
|
event: MXEvent,
|
|
pushRule: MXPushRule?,
|
|
onComplete: @escaping (UNNotificationContent?, Bool) -> Void
|
|
) {
|
|
|
|
var validatedNotificationBody: String? = notificationBody
|
|
var validatedNotificationTitle: String? = notificationTitle
|
|
if self.localAuthenticationService.isProtectionSet {
|
|
MXLog.debug("[NotificationService] validateNotificationContentAndComplete: Resetting title and body because app protection is set")
|
|
validatedNotificationBody = NotificationService.localizedString(forKey: "MESSAGE_PROTECTED")
|
|
validatedNotificationTitle = nil
|
|
}
|
|
|
|
guard validatedNotificationBody != nil else {
|
|
MXLog.debug("[NotificationService] validateNotificationContentAndComplete: notificationBody is nil")
|
|
onComplete(nil, false)
|
|
return
|
|
}
|
|
|
|
let notificationContent = self.notificationContent(withTitle: validatedNotificationTitle,
|
|
body: validatedNotificationBody,
|
|
threadIdentifier: threadIdentifier,
|
|
userId: currentUserId,
|
|
event: event,
|
|
pushRule: pushRule,
|
|
additionalInfo: additionalUserInfo)
|
|
|
|
MXLog.debug("[NotificationService] validateNotificationContentAndComplete: Calling onComplete.")
|
|
onComplete(notificationContent, ignoreBadgeUpdate)
|
|
}
|
|
|
|
/// Returns the default title for message notifications.
|
|
/// - Parameters:
|
|
/// - eventSenderName: The displayname of the sender.
|
|
/// - roomDisplayName: The displayname of the room the message was sent in.
|
|
/// - Returns: A string to be used for the notification's title.
|
|
private func messageTitle(for eventSenderName: String, in roomDisplayName: String?) -> String {
|
|
// Display the room name only if it is different than the sender name
|
|
if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName {
|
|
return NotificationService.localizedString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", eventSenderName, roomDisplayName)
|
|
} else {
|
|
return eventSenderName
|
|
}
|
|
}
|
|
|
|
private func replyTitle(for eventSenderName: String, in roomDisplayName: String?) -> String {
|
|
// Display the room name only if it is different than the sender name
|
|
if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName {
|
|
return NotificationService.localizedString(forKey: "REPLY_FROM_USER_IN_ROOM_TITLE", eventSenderName, roomDisplayName)
|
|
} else {
|
|
return NotificationService.localizedString(forKey: "REPLY_FROM_USER_TITLE", eventSenderName)
|
|
}
|
|
}
|
|
|
|
/// Get the context of an event.
|
|
/// - Parameters:
|
|
/// - event: the event
|
|
/// - roomId: the id of the room of the event.
|
|
/// - completion: Completion block that will return the room state and the sender display name.
|
|
private func context(ofEvent event: MXEvent, inRoom roomId: String,
|
|
completion: @escaping (MXResponse<(MXRoomState, String)>) -> Void) {
|
|
// First get the room state
|
|
NotificationService.backgroundSyncService.roomState(forRoomId: roomId) { (response) in
|
|
switch response {
|
|
case .success(let roomState):
|
|
// Extract the member name from room state member
|
|
let eventSender = event.sender!
|
|
let eventSenderName = roomState.members.memberName(eventSender) ?? eventSender
|
|
|
|
// Check if we are happy with it
|
|
if eventSenderName != eventSender
|
|
|| roomState.members.member(withUserId: eventSender) != nil {
|
|
completion(.success((roomState, eventSenderName)))
|
|
return
|
|
}
|
|
|
|
// Else, if the room member is not known, use the user profile to avoid to display a Matrix id
|
|
NotificationService.backgroundSyncService.profile(ofMember: eventSender, inRoom: roomId) { (response) in
|
|
switch response {
|
|
case .success((let displayName, _)):
|
|
guard let displayName = displayName else {
|
|
completion(.success((roomState, eventSender)))
|
|
return
|
|
}
|
|
completion(.success((roomState, displayName)))
|
|
|
|
case .failure(_):
|
|
completion(.success((roomState, eventSender)))
|
|
}
|
|
}
|
|
case .failure(let error):
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func notificationContent(withTitle title: String?,
|
|
body: String?,
|
|
threadIdentifier: String?,
|
|
userId: String?,
|
|
event: MXEvent,
|
|
pushRule: MXPushRule?,
|
|
additionalInfo: [AnyHashable: Any]? = nil) -> UNNotificationContent {
|
|
let notificationContent = UNMutableNotificationContent()
|
|
|
|
if let title = title {
|
|
notificationContent.title = title
|
|
}
|
|
if let body = body {
|
|
notificationContent.body = body
|
|
}
|
|
if let threadIdentifier = threadIdentifier {
|
|
notificationContent.threadIdentifier = threadIdentifier
|
|
}
|
|
if let categoryIdentifier = self.notificationCategoryIdentifier(forEvent: event) {
|
|
notificationContent.categoryIdentifier = categoryIdentifier
|
|
}
|
|
if let soundName = notificationSoundName(fromPushRule: pushRule) {
|
|
notificationContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: soundName))
|
|
}
|
|
notificationContent.userInfo = notificationUserInfo(forEvent: event,
|
|
andUserId: userId,
|
|
additionalInfo: additionalInfo)
|
|
|
|
return notificationContent
|
|
}
|
|
|
|
private func notificationUserInfo(forEvent event: MXEvent,
|
|
andUserId userId: String?,
|
|
additionalInfo: [AnyHashable: Any]? = nil) -> [AnyHashable: Any] {
|
|
var notificationUserInfo: [AnyHashable: Any] = [
|
|
"type": "full",
|
|
"room_id": event.roomId as Any,
|
|
"event_id": event.eventId as Any
|
|
]
|
|
if let threadId = event.threadId {
|
|
notificationUserInfo["thread_id"] = threadId
|
|
}
|
|
if let userId = userId {
|
|
notificationUserInfo["user_id"] = userId
|
|
}
|
|
if let additionalInfo = additionalInfo {
|
|
for (key, value) in additionalInfo {
|
|
notificationUserInfo[key] = value
|
|
}
|
|
}
|
|
return notificationUserInfo
|
|
}
|
|
|
|
private func notificationSoundName(fromPushRule pushRule: MXPushRule?) -> String? {
|
|
var soundName: String?
|
|
|
|
// Set sound name based on the value provided in action of MXPushRule
|
|
for ruleAction in pushRule?.actions ?? [] {
|
|
guard let action = ruleAction as? MXPushRuleAction else { continue }
|
|
guard action.actionType == MXPushRuleActionTypeSetTweak else { continue }
|
|
guard action.parameters["set_tweak"] as? String == "sound" else { continue }
|
|
soundName = action.parameters["value"] as? String
|
|
if soundName == "default" {
|
|
soundName = "message.caf"
|
|
}
|
|
}
|
|
|
|
MXLog.debug("Sound name: \(String(describing: soundName))")
|
|
|
|
return soundName
|
|
}
|
|
|
|
private func notificationCategoryIdentifier(forEvent event: MXEvent) -> String? {
|
|
let isNotificationContentShown = (!event.isEncrypted || self.showDecryptedContentInNotifications)
|
|
&& !localAuthenticationService.isProtectionSet
|
|
|
|
guard isNotificationContentShown else {
|
|
return Constants.toBeRemovedNotificationCategoryIdentifier
|
|
}
|
|
|
|
if event.eventType == .callInvite {
|
|
return Constants.callInviteNotificationCategoryIdentifier
|
|
}
|
|
|
|
guard event.eventType == .roomMessage || event.eventType == .roomEncrypted else {
|
|
return Constants.toBeRemovedNotificationCategoryIdentifier
|
|
}
|
|
|
|
return "QUICK_REPLY"
|
|
}
|
|
|
|
/// Attempts to send trigger a VoIP push for the given event
|
|
/// - Parameter event: The call invite event.
|
|
private func sendVoipPush(forEvent event: MXEvent) {
|
|
guard let token = pushNotificationStore.pushKitToken else {
|
|
return
|
|
}
|
|
|
|
if #available(iOS 13.0, *) {
|
|
if event.isEncrypted {
|
|
guard let clearEvent = event.clear else {
|
|
MXLog.debug("[NotificationService] sendVoipPush: Do not send a VoIP push for undecrypted event, it'll cause a crash.")
|
|
return
|
|
}
|
|
|
|
// Add some original data on the clear event
|
|
clearEvent.eventId = event.eventId
|
|
clearEvent.originServerTs = event.originServerTs
|
|
clearEvent.sender = event.sender
|
|
clearEvent.roomId = event.roomId
|
|
pushNotificationStore.storeCallInvite(clearEvent)
|
|
} else {
|
|
pushNotificationStore.storeCallInvite(event)
|
|
}
|
|
}
|
|
|
|
ongoingVoIPPushRequests[event.eventId] = true
|
|
|
|
let appId = BuildSettings.pushKitAppId
|
|
|
|
pushGatewayRestClient.notifyApp(withId: appId,
|
|
pushToken: token,
|
|
eventId: event.eventId,
|
|
roomId: event.roomId,
|
|
eventType: nil,
|
|
sender: event.sender,
|
|
timeout: NSE.Constants.voipPushRequestTimeout,
|
|
success: { [weak self] (rejected) in
|
|
MXLog.debug("[NotificationService] sendVoipPush succeeded, rejected tokens: \(rejected)")
|
|
|
|
guard let self = self else { return }
|
|
self.ongoingVoIPPushRequests.removeValue(forKey: event.eventId)
|
|
|
|
self.fallbackToBestAttemptContent(forEventId: event.eventId)
|
|
}) { [weak self] (error) in
|
|
MXLog.debug("[NotificationService] sendVoipPush failed with error: \(error)")
|
|
|
|
guard let self = self else { return }
|
|
self.ongoingVoIPPushRequests.removeValue(forKey: event.eventId)
|
|
|
|
self.fallbackToBestAttemptContent(forEventId: event.eventId)
|
|
}
|
|
}
|
|
|
|
private func sendReadReceipt(forEvent event: MXEvent) {
|
|
guard let mxRestClient = mxRestClient else {
|
|
MXLog.error("[NotificationService] sendReadReceipt: Missing mxRestClient for read receipt request.")
|
|
return
|
|
}
|
|
guard let eventId = event.eventId,
|
|
let roomId = event.roomId else {
|
|
MXLog.error("[NotificationService] sendReadReceipt: Event information missing for read receipt request.")
|
|
return
|
|
}
|
|
|
|
mxRestClient.sendReadReceipt(toRoom: roomId, forEvent: eventId, threadId: event.threadId) { response in
|
|
if response.isSuccess {
|
|
MXLog.debug("[NotificationService] sendReadReceipt: Read receipt send successfully.")
|
|
} else if let error = response.error {
|
|
MXLog.error("[NotificationService] sendReadReceipt: Read receipt send failed", context: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func localizedString(forKey key: String, _ args: CVarArg...) -> String {
|
|
// The bundle needs to be an MXKLanguageBundle and contain the lproj files.
|
|
// MatrixKit now sets the app bundle as the MXKLanguageBundle
|
|
let format = NSLocalizedString(key, bundle: Bundle.app, comment: "")
|
|
let locale = LocaleProvider.locale ?? Locale.current
|
|
|
|
return String(format: format, locale: locale, arguments: args)
|
|
}
|
|
}
|