element-ios/RiotNSE/NotificationService.swift

903 lines
47 KiB
Swift
Raw Normal View History

/*
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
2020-08-10 11:03:47 +00:00
import MatrixSDK
2021-05-10 12:21:30 +00:00
/// The number of milliseconds in one second.
private let MSEC_PER_SEC: TimeInterval = 1000
class NotificationService: UNNotificationServiceExtension {
2021-05-10 11:47:18 +00:00
private struct NSE {
enum Constants {
static let voipPushRequestTimeout: TimeInterval = 15
static let timeNeededToSendVoIPPushes: TimeInterval = 20
2021-05-10 11:47:18 +00:00
}
}
2020-12-18 11:14:35 +00:00
// MARK: - Properties
2021-05-10 14:29:24 +00:00
/// Receiving dates for notifications. Keys are eventId's
2021-05-10 12:21:30 +00:00
private var receiveDates: [String: Date] = [:]
/// Content handlers. Keys are eventId's
2020-10-22 14:27:35 +00:00
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] = [:]
2020-10-22 14:27:35 +00:00
private var userAccount: MXKAccount?
private var isCryptoSDKEnabled = false
/// 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
2020-10-22 14:27:35 +00:00
private var bestAttemptContents: [String: UNMutableNotificationContent] = [:]
2020-10-22 14:27:35 +00:00
private static var backgroundSyncService: MXBackgroundSyncService!
private var showDecryptedContentInNotifications: Bool {
return RiotSettings.shared.showDecryptedContentInNotifications
}
2020-10-22 14:27:35 +00:00
private lazy var configuration: Configurable = {
return CommonConfiguration()
2020-07-29 08:03:37 +00:00
}()
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
}()
2020-10-22 14:27:35 +00:00
private static var isLoggerInitialized: Bool = false
2020-08-10 11:03:47 +00:00
private lazy var pushGatewayRestClient: MXPushGatewayRestClient = {
let url = URL(string: BuildSettings.serverConfigSygnalAPIUrlString)!
2020-08-10 15:17:13 +00:00
return MXPushGatewayRestClient(pushGateway: url.scheme! + "://" + url.host!, andOnUnrecognizedCertificateBlock: nil)
2020-08-10 11:03:47 +00:00
}()
2020-08-20 14:43:46 +00:00
private var pushNotificationStore: PushNotificationStore = PushNotificationStore()
private let localAuthenticationService = LocalAuthenticationService(pinCodePreferences: .shared)
private static let backgroundServiceInitQueue = DispatchQueue(label: "io.element.NotificationService.backgroundServiceInitQueue")
2020-12-18 11:14:35 +00:00
// 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) {
2021-01-21 11:22:19 +00:00
let userInfo = request.content.userInfo
2020-07-28 14:53:55 +00:00
// Set static application settings
2020-07-29 08:03:37 +00:00
configuration.setupSettings()
2020-06-02 15:25:19 +00:00
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)
}
2020-06-16 13:17:58 +00:00
// setup logs
setupLogger()
2020-06-17 12:18:20 +00:00
MXLog.debug(" ")
MXLog.debug(" ")
MXLog.debug("################################################################################")
MXLog.debug("[NotificationService] Instance: \(self), thread: \(Thread.current)")
MXLog.debug("[NotificationService] Payload came: \(userInfo)")
2020-12-18 12:03:00 +00:00
// log memory at the beginning of the process
2020-12-18 11:14:35 +00:00
logMemory()
setupAnalytics()
UNUserNotificationCenter.current().removeUnwantedNotifications()
2020-06-02 15:25:19 +00:00
// 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
}
2020-06-25 08:48:02 +00:00
2021-05-10 12:21:30 +00:00
// store receive date
receiveDates[eventId] = Date()
2020-06-25 08:48:02 +00:00
// 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.
2020-06-11 13:46:02 +00:00
MXLog.debug("[NotificationService] serviceExtensionTimeWillExpire")
2020-06-11 13:46:02 +00:00
// 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.
}
2021-01-21 11:22:19 +00:00
deinit {
MXLog.debug("[NotificationService] deinit for \(self)");
self.logMemory()
MXLog.debug(" ")
2021-01-21 11:22:19 +00:00
}
2020-12-18 11:14:35 +00:00
// MARK: - Private
private func logMemory() {
MXLog.debug("[NotificationService] Memory: footprint: \(MXMemory.formattedMemoryFootprint()) - available: \(MXMemory.formattedMemoryAvailable())")
2020-12-18 11:14:35 +00:00
}
private func setupLogger() {
2020-10-12 18:52:43 +00:00
if !NotificationService.isLoggerInitialized {
let configuration = MXLogConfiguration()
configuration.logLevel = .verbose
configuration.maxLogFilesCount = 100
configuration.logFilesSizeLimit = 10 * 1024 * 1024; // 10MB
configuration.subLogName = "nse"
2020-10-12 18:52:43 +00:00
if isatty(STDERR_FILENO) == 0 {
configuration.redirectLogsToFiles = true
2020-10-12 18:52:43 +00:00
}
MXLog.configure(configuration)
2020-10-12 18:52:43 +00:00
NotificationService.isLoggerInitialized = true
}
2020-06-02 15:25:19 +00:00
}
private func setupAnalytics(){
// Configure our analytics. It will start if the option is enabled
let analytics = Analytics.shared
MXSDKOptions.sharedInstance().analyticsDelegate = analytics
analytics.startIfEnabled()
}
2020-12-18 11:14:35 +00:00
private func setup(withRoomId roomId: String, eventId: String, completion: @escaping () -> Void) {
MXKAccountManager.sharedManager(withReload: true)
2020-10-22 14:27:35 +00:00
self.userAccount = MXKAccountManager.shared()?.activeAccounts.first
if let userAccount = userAccount {
Self.backgroundServiceInitQueue.sync {
if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials {
MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE")
self.logMemory()
2022-10-31 12:23:52 +00:00
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()
2020-06-11 13:37:21 +00:00
}
2020-06-02 15:25:19 +00:00
} else {
MXLog.debug("[NotificationService] setup: No active accounts")
2020-06-25 11:23:21 +00:00
fallbackToBestAttemptContent(forEventId: eventId)
}
}
/// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require
/// rebuilding `MXBackgroundSyncService`
private func hasChangedCryptoSDK() -> Bool {
2022-10-31 12:23:52 +00:00
guard isCryptoSDKEnabled != RiotSettings.shared.enableCryptoSDK else {
return false
}
2022-10-31 12:23:52 +00:00
isCryptoSDKEnabled = RiotSettings.shared.enableCryptoSDK
return true
}
/// 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
2020-12-18 11:14:35 +00:00
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.
2020-10-22 14:27:35 +00:00
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):
2022-08-22 09:20:36 +00:00
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):
2022-08-22 09:20:36 +00:00
MXLog.error("[NotificationService] checkPlaybackAndContinueProcessing: Failed fetching read marker event", context: error)
self?.processEvent(notificationEvent)
}
}
}
2020-10-22 14:27:35 +00:00
private func processEvent(_ event: MXEvent) {
2021-05-10 12:21:30 +00:00
if let receiveDate = receiveDates[event.eventId] {
MXLog.debug("[NotificationService] processEvent: notification receive delay: \(receiveDate.timeIntervalSince1970*MSEC_PER_SEC - TimeInterval(event.originServerTs)) ms")
2021-05-10 12:21:30 +00:00
}
2020-10-22 14:27:35 +00:00
guard let content = bestAttemptContents[event.eventId], let userAccount = userAccount else {
2020-06-25 11:23:21 +00:00
self.fallbackToBestAttemptContent(forEventId: event.eventId)
return
}
2020-12-18 11:14:35 +00:00
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())
}
2020-10-22 14:27:35 +00:00
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)
2020-12-09 11:01:45 +00:00
// clear maps
contentHandlers.removeValue(forKey: eventId)
bestAttemptContents.removeValue(forKey: eventId)
2021-05-10 12:21:30 +00:00
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
}
2020-10-22 14:27:35 +00:00
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")
2020-10-22 14:27:35 +00:00
self.context(ofEvent: event, inRoom: roomId, completion: { (response) in
2020-10-22 14:27:35 +00:00
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)
// if the push rule must not be notified we complete and return
if pushRule?.dontNotify == true {
onComplete(nil, false)
return
}
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
2021-05-11 13:17:25 +00:00
} 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:
2021-08-04 15:14:02 +00:00
if event.isVoiceMessage() {
// Ignore voice broadcast chunk event except the first one.
if let chunkInfo = event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] as? [String: UInt] {
if chunkInfo[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkSequence] == 1 {
notificationBody = NotificationService.localizedString(forKey: "VOICE_BROADCAST_FROM_USER", eventSenderName)
}
} else {
notificationBody = NotificationService.localizedString(forKey: "VOICE_MESSAGE_FROM_USER", eventSenderName)
}
2021-08-04 15:14:02 +00:00
} else {
notificationBody = NotificationService.localizedString(forKey: "AUDIO_FROM_USER", eventSenderName, messageContent)
2021-08-04 15:14:02 +00:00
}
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.
2021-08-17 14:55:35 +00:00
// 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)
2021-08-17 14:38:01 +00:00
// 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 {
2021-08-17 14:38:01 +00:00
// 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 {
2021-08-17 14:38:01 +00:00
// If the display name hasn't changed, handle as an avatar change.
notificationBody = NotificationService.localizedString(forKey: "USER_UPDATED_AVATAR", eventSenderName)
}
} else {
2021-08-17 14:38:01 +00:00
// 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.
2021-08-17 14:55:35 +00:00
// 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)
}
}
2021-08-17 14:55:35 +00:00
case .sticker:
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
notificationBody = NotificationService.localizedString(forKey: "STICKER_FROM_USER", eventSenderName)
2021-08-17 14:55:35 +00:00
// 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:
2021-04-29 09:34:27 +00:00
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")
2021-04-29 09:34:27 +00:00
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
case .pollEnd:
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
notificationBody = VectorL10n.pollTimelineEndedText
2020-10-22 14:27:35 +00:00
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)
}
2020-10-22 14:27:35 +00:00
})
}
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))
}
}
}
2020-12-18 11:14:35 +00:00
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
]
2022-03-08 09:58:13 +00:00
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
}
2020-12-18 11:14:35 +00:00
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))")
2020-06-19 14:07:13 +00:00
return soundName
}
2020-12-18 11:14:35 +00:00
private func notificationCategoryIdentifier(forEvent event: MXEvent) -> String? {
let isNotificationContentShown = (!event.isEncrypted || self.showDecryptedContentInNotifications)
&& !localAuthenticationService.isProtectionSet
guard isNotificationContentShown else {
return Constants.toBeRemovedNotificationCategoryIdentifier
}
2020-08-10 15:17:46 +00:00
if event.eventType == .callInvite {
return Constants.callInviteNotificationCategoryIdentifier
2020-08-10 15:17:46 +00:00
}
guard event.eventType == .roomMessage || event.eventType == .roomEncrypted else {
return Constants.toBeRemovedNotificationCategoryIdentifier
}
return "QUICK_REPLY"
}
2020-08-11 18:17:48 +00:00
/// Attempts to send trigger a VoIP push for the given event
/// - Parameter event: The call invite event.
2020-08-10 11:03:47 +00:00
private func sendVoipPush(forEvent event: MXEvent) {
2020-08-20 14:33:03 +00:00
guard let token = pushNotificationStore.pushKitToken else {
2020-08-10 11:03:47 +00:00
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.")
2021-05-10 14:29:24 +00:00
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)
2021-05-10 14:29:24 +00:00
}
}
2020-08-11 18:17:48 +00:00
ongoingVoIPPushRequests[event.eventId] = true
2020-08-10 15:17:13 +00:00
let appId = BuildSettings.pushKitAppId
2021-05-10 11:47:18 +00:00
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)")
2021-05-10 11:47:18 +00:00
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)
2020-08-10 11:03:47 +00:00
}
}
private func sendReadReceipt(forEvent event: MXEvent) {
guard let mxRestClient = mxRestClient else {
2021-11-16 12:24:21 +00:00
MXLog.error("[NotificationService] sendReadReceipt: Missing mxRestClient for read receipt request.")
return
}
guard let eventId = event.eventId,
let roomId = event.roomId else {
2021-11-16 12:24:21 +00:00
MXLog.error("[NotificationService] sendReadReceipt: Event information missing for read receipt request.")
return
}
Release 1.9.12 (#7081) * 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 commit 81773cd1e515cc391c1f21b499f61141cb03c810. * Revert "Fix timeline items text height calculation" This reverts commit 8f9eddee501702de84192316bd5b2ff9512d681a. * Revert "Fixes vector-im/element-ios/issues/6441 - Incorrect timeline item text height calculation (#6679)" This reverts commit 405c2d8e324c08c1a40e037aeb3c54e93f30bc9f. * 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 commit e6367cba4c8f0cb2cdfe5e3381dcbb7bc0f94c52. * 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>
2022-11-15 13:40:36 +00:00
mxRestClient.sendReadReceipt(toRoom: roomId, forEvent: eventId, threadId: event.threadId) { response in
if response.isSuccess {
2021-11-16 12:24:21 +00:00
MXLog.debug("[NotificationService] sendReadReceipt: Read receipt send successfully.")
} else if let error = response.error {
2022-08-22 09:20:36 +00:00
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)
}
}
private extension MXPushRule {
var dontNotify: Bool {
let actions = (actions as? [MXPushRuleAction]) ?? []
return actions.contains { $0.actionType == MXPushRuleActionTypeDontNotify }
}
}