mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Notifications (#275)
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
//
|
||||
// Copyright 2022 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 Intents
|
||||
import MatrixRustSDK
|
||||
import UserNotifications
|
||||
|
||||
class NotificationServiceExtension: UNNotificationServiceExtension {
|
||||
private lazy var keychainController = KeychainController(service: .sessions,
|
||||
accessGroup: InfoPlistReader.target.appGroupIdentifier)
|
||||
var handler: ((UNNotificationContent) -> Void)?
|
||||
var modifiedContent: UNMutableNotificationContent?
|
||||
|
||||
override init() {
|
||||
// Use `en` as fallback language
|
||||
Bundle.elementFallbackLanguage = "en"
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest,
|
||||
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory),
|
||||
let roomId = request.roomId,
|
||||
let eventId = request.eventId,
|
||||
let credentials = keychainController.restorationTokens().first else {
|
||||
// We cannot process this notification, it might be due to one of these:
|
||||
// - Device rebooted and locked
|
||||
// - Not a Matrix notification
|
||||
// - User is not signed in
|
||||
return contentHandler(request.content)
|
||||
}
|
||||
|
||||
handler = contentHandler
|
||||
modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent
|
||||
|
||||
NSELogger.configure()
|
||||
|
||||
NSELogger.logMemory(with: tag)
|
||||
|
||||
MXLog.debug("\(tag) #########################################")
|
||||
MXLog.debug("\(tag) Payload came: \(request.content.userInfo)")
|
||||
|
||||
Task {
|
||||
try await run(with: credentials,
|
||||
roomId: roomId,
|
||||
eventId: eventId)
|
||||
}
|
||||
}
|
||||
|
||||
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("\(tag) serviceExtensionTimeWillExpire")
|
||||
notify()
|
||||
}
|
||||
|
||||
private func run(with credentials: KeychainCredentials,
|
||||
roomId: String,
|
||||
eventId: String) async throws {
|
||||
MXLog.debug("\(tag) run with roomId: \(roomId), eventId: \(eventId)")
|
||||
|
||||
let service = NotificationServiceProxy(basePath: URL.sessionsBaseDirectory.path,
|
||||
userId: credentials.userID)
|
||||
|
||||
guard let itemProxy = try await service.notificationItem(roomId: roomId,
|
||||
eventId: eventId) else {
|
||||
MXLog.debug("\(tag) got no notification item")
|
||||
|
||||
// Notification should be discarded
|
||||
return discard()
|
||||
}
|
||||
|
||||
// First process without a media proxy.
|
||||
// After this some properties of the notification should be set, like title, subtitle, sound etc.
|
||||
guard let firstContent = try await itemProxy.process(with: roomId,
|
||||
mediaProvider: nil) else {
|
||||
MXLog.debug("\(tag) not even first content")
|
||||
|
||||
// Notification should be discarded
|
||||
return discard()
|
||||
}
|
||||
|
||||
// After the first processing, update the modified content
|
||||
modifiedContent = firstContent
|
||||
|
||||
guard itemProxy.requiresMediaProvider else {
|
||||
MXLog.debug("\(tag) no media needed")
|
||||
|
||||
// We've processed the item and no media operations needed, so no need to go further
|
||||
return notify()
|
||||
}
|
||||
|
||||
MXLog.debug("\(tag) process with media")
|
||||
|
||||
// There is some media to load, process it again
|
||||
if let latestContent = try await itemProxy.process(with: roomId,
|
||||
mediaProvider: try createMediaProvider(with: credentials)) {
|
||||
// Processing finished, hopefully with some media
|
||||
modifiedContent = latestContent
|
||||
return notify()
|
||||
} else {
|
||||
// This is not very likely, as it should discard the notification sooner
|
||||
return discard()
|
||||
}
|
||||
}
|
||||
|
||||
private func createMediaProvider(with credentials: KeychainCredentials) throws -> MediaProviderProtocol {
|
||||
let builder = ClientBuilder()
|
||||
.basePath(path: URL.sessionsBaseDirectory.path)
|
||||
.username(username: credentials.userID)
|
||||
|
||||
let client = try builder.build()
|
||||
try client.restoreSession(session: credentials.restorationToken.session)
|
||||
|
||||
MXLog.debug("\(tag) creating media provider")
|
||||
|
||||
return MediaProvider(mediaProxy: MediaProxy(client: client),
|
||||
imageCache: .onlyOnDisk,
|
||||
fileCache: .default,
|
||||
backgroundTaskService: nil)
|
||||
}
|
||||
|
||||
private func notify() {
|
||||
MXLog.debug("\(tag) notify")
|
||||
|
||||
guard let modifiedContent else {
|
||||
MXLog.debug("\(tag) notify: no modified content")
|
||||
return
|
||||
}
|
||||
handler?(modifiedContent)
|
||||
handler = nil
|
||||
self.modifiedContent = nil
|
||||
}
|
||||
|
||||
private func discard() {
|
||||
MXLog.debug("\(tag) discard")
|
||||
|
||||
handler?(UNMutableNotificationContent())
|
||||
handler = nil
|
||||
modifiedContent = nil
|
||||
}
|
||||
|
||||
private var tag: String {
|
||||
"[NSE][\(Unmanaged.passUnretained(self).toOpaque())][\(Unmanaged.passUnretained(Thread.current).toOpaque())]"
|
||||
}
|
||||
|
||||
deinit {
|
||||
NSELogger.logMemory(with: tag)
|
||||
MXLog.debug("\(tag) deinit")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
|
||||
final class DataProtectionManager {
|
||||
/// Detects after reboot, before unlocked state. Does this by trying to write a file to the filesystem (to the Caches directory) and read it back.
|
||||
/// - Parameter containerURL: Container url to write the file.
|
||||
/// - Returns: true if the state detected
|
||||
static func isDeviceLockedAfterReboot(containerURL: URL) -> Bool {
|
||||
let dummyString = ProcessInfo.processInfo.globallyUniqueString
|
||||
guard let dummyData = dummyString.data(using: .utf8) else {
|
||||
return true
|
||||
}
|
||||
|
||||
do {
|
||||
// add a unique filename
|
||||
let url = containerURL.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString)
|
||||
|
||||
try dummyData.write(to: url, options: .completeFileProtectionUntilFirstUserAuthentication)
|
||||
let readData = try Data(contentsOf: url)
|
||||
let readString = String(data: readData, encoding: .utf8)
|
||||
try FileManager.default.removeItem(at: url)
|
||||
if readString != dummyString {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
class NSELogger {
|
||||
private static var isConfigured = false
|
||||
|
||||
/// Memory formatter, uses exact 2 fraction digits and no grouping
|
||||
private static var numberFormatter: NumberFormatter {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.alwaysShowsDecimalSeparator = true
|
||||
formatter.decimalSeparator = "."
|
||||
formatter.groupingSeparator = ""
|
||||
formatter.maximumFractionDigits = 2
|
||||
formatter.minimumFractionDigits = 2
|
||||
return formatter
|
||||
}
|
||||
|
||||
private static var formattedMemoryAvailable: String {
|
||||
let freeBytes = os_proc_available_memory()
|
||||
let freeMB = Double(freeBytes) / 1024 / 1024
|
||||
guard let formattedStr = numberFormatter.string(from: NSNumber(value: freeMB)) else {
|
||||
return ""
|
||||
}
|
||||
return "\(formattedStr) MB"
|
||||
}
|
||||
|
||||
/// Details: https://developer.apple.com/forums/thread/105088
|
||||
/// - Returns: Current memory footprint
|
||||
private static var memoryFootprint: Float? {
|
||||
// The `TASK_VM_INFO_COUNT` and `TASK_VM_INFO_REV1_COUNT` macros are too
|
||||
// complex for the Swift C importer, so we have to define them ourselves.
|
||||
let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<integer_t>.size)
|
||||
guard let offset = MemoryLayout.offset(of: \task_vm_info_data_t.min_address) else {
|
||||
return nil
|
||||
}
|
||||
let TASK_VM_INFO_REV1_COUNT = mach_msg_type_number_t(offset / MemoryLayout<integer_t>.size)
|
||||
var info = task_vm_info_data_t()
|
||||
var count = TASK_VM_INFO_COUNT
|
||||
let kr = withUnsafeMutablePointer(to: &info) { infoPtr in
|
||||
infoPtr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in
|
||||
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), intPtr, &count)
|
||||
}
|
||||
}
|
||||
guard kr == KERN_SUCCESS, count >= TASK_VM_INFO_REV1_COUNT else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Float(info.phys_footprint)
|
||||
}
|
||||
|
||||
/// Formatted memory footprint for debugging purposes
|
||||
/// - Returns: Memory footprint in MBs as a readable string
|
||||
public static var formattedMemoryFootprint: String {
|
||||
let usedBytes = UInt64(memoryFootprint ?? 0)
|
||||
let usedMB = Double(usedBytes) / 1024 / 1024
|
||||
guard let formattedStr = numberFormatter.string(from: NSNumber(value: usedMB)) else {
|
||||
return ""
|
||||
}
|
||||
return "\(formattedStr) MB"
|
||||
}
|
||||
|
||||
static func configure() {
|
||||
guard !isConfigured else {
|
||||
return
|
||||
}
|
||||
isConfigured = true
|
||||
|
||||
let configuration = MXLogConfiguration()
|
||||
configuration.maxLogFilesCount = 10
|
||||
configuration.subLogName = "nse"
|
||||
|
||||
#if DEBUG
|
||||
// This exposes the full Rust side tracing subscriber filter for more flexibility.
|
||||
// We can filter by level, crate and even file. See more details here:
|
||||
// https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples
|
||||
setupTracing(filter: "warn,hyper=warn,sled=warn,matrix_sdk_sled=warn")
|
||||
configuration.logLevel = .debug
|
||||
#else
|
||||
setupTracing(filter: "info,hyper=warn,sled=warn,matrix_sdk_sled=warn")
|
||||
configuration.logLevel = .info
|
||||
#endif
|
||||
|
||||
// Avoid redirecting NSLogs to files if we are attached to a debugger.
|
||||
if isatty(STDERR_FILENO) == 0 {
|
||||
configuration.redirectLogsToFiles = true
|
||||
}
|
||||
|
||||
MXLog.configure(configuration)
|
||||
}
|
||||
|
||||
static func logMemory(with tag: String) {
|
||||
MXLog.debug("\(tag) Memory: footprint: \(formattedMemoryFootprint) - available: \(formattedMemoryAvailable)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
import MatrixRustSDK
|
||||
import UserNotifications
|
||||
|
||||
extension NotificationItemProxy {
|
||||
var requiresMediaProvider: Bool {
|
||||
false
|
||||
// if avatarUrl != nil {
|
||||
// return true
|
||||
// }
|
||||
// switch timelineItemProxy {
|
||||
// case .event(let eventItem):
|
||||
// guard eventItem.isMessage else {
|
||||
// // To be handled in the future
|
||||
// return false
|
||||
// }
|
||||
// guard let message = eventItem.content.asMessage() else {
|
||||
// fatalError("Only handled messages")
|
||||
// }
|
||||
// switch message.msgtype() {
|
||||
// case .image, .video:
|
||||
// return true
|
||||
// default:
|
||||
// return false
|
||||
// }
|
||||
// case .virtual:
|
||||
// return false
|
||||
// case .other:
|
||||
// return false
|
||||
// }
|
||||
}
|
||||
|
||||
/// Process the receiver item proxy
|
||||
/// - Parameters:
|
||||
/// - roomId: Room identifier
|
||||
/// - mediaProvider: Media provider to process also media. May be passed nil to ignore media operations.
|
||||
/// - Returns: A notification content object if the notification should be displayed. Otherwise nil.
|
||||
func process(with roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? {
|
||||
nil
|
||||
// switch timelineItemProxy {
|
||||
// case .event(let eventItem):
|
||||
// guard eventItem.isMessage else {
|
||||
// // To be handled in the future
|
||||
// return nil
|
||||
// }
|
||||
// guard let message = eventItem.content.asMessage() else {
|
||||
// fatalError("Item must be a message")
|
||||
// }
|
||||
//
|
||||
// return try await process(message: message,
|
||||
// senderId: eventItem.sender,
|
||||
// roomId: roomId,
|
||||
// mediaProvider: mediaProvider)
|
||||
// case .virtual:
|
||||
// return nil
|
||||
// case .other:
|
||||
// return nil
|
||||
// }
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
// MARK: Common
|
||||
|
||||
private func process(message: Message,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? {
|
||||
switch message.msgtype() {
|
||||
case .text(content: let content):
|
||||
return try await processText(content: content,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
case .image(content: let content):
|
||||
return try await processImage(content: content,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
case .video(content: let content):
|
||||
return try await processVideo(content: content,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
case .file(content: let content):
|
||||
return try await processFile(content: content,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
case .notice(content: let content):
|
||||
return try await processNotice(content: content,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
case .emote(content: let content):
|
||||
return try await processEmote(content: content,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
case .none:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func processCommon(senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
var notification = UNMutableNotificationContent()
|
||||
notification.title = title
|
||||
if let subtitle = subtitle {
|
||||
notification.subtitle = subtitle
|
||||
}
|
||||
notification.threadIdentifier = roomId
|
||||
notification.categoryIdentifier = NotificationConstants.Category.reply
|
||||
notification.sound = isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil
|
||||
|
||||
notification = try await notification.addSenderIcon(using: mediaProvider,
|
||||
senderId: senderId,
|
||||
senderName: title,
|
||||
mediaSource: avatarMediaSource,
|
||||
roomId: roomId)
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
// MARK: Message Types
|
||||
|
||||
private func processText(content: TextMessageContent,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
let notification = try await processCommon(senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
notification.body = content.body
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
private func processImage(content: ImageMessageContent,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
var notification = try await processCommon(senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
notification.body = "📷 " + content.body
|
||||
|
||||
notification = try await notification.addMediaAttachment(using: mediaProvider,
|
||||
mediaSource: .init(source: content.source))
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
private func processVideo(content: VideoMessageContent,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
var notification = try await processCommon(senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
notification.body = "📹 " + content.body
|
||||
|
||||
notification = try await notification.addMediaAttachment(using: mediaProvider,
|
||||
mediaSource: .init(source: content.source))
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
private func processFile(content: FileMessageContent,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
let notification = try await processCommon(senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
notification.body = "📄 " + content.body
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
private func processNotice(content: NoticeMessageContent,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
let notification = try await processCommon(senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
notification.body = "❕ " + content.body
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
private func processEmote(content: EmoteMessageContent,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
let notification = try await processCommon(senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
notification.body = "🫥 " + content.body
|
||||
|
||||
return notification
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
import Intents
|
||||
import UserNotifications
|
||||
|
||||
extension UNMutableNotificationContent {
|
||||
func addMediaAttachment(using mediaProvider: MediaProviderProtocol?,
|
||||
mediaSource: MediaSourceProxy) async throws -> UNMutableNotificationContent {
|
||||
guard let mediaProvider else {
|
||||
return self
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadFileFromSource(mediaSource, fileExtension: "") {
|
||||
case .success(let url):
|
||||
let attachment = try UNNotificationAttachment(identifier: ProcessInfo.processInfo.globallyUniqueString,
|
||||
url: url,
|
||||
options: nil)
|
||||
attachments.append(attachment)
|
||||
case .failure(let error):
|
||||
MXLog.debug("Couldn't add media attachment: \(error)")
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func addSenderIcon(using mediaProvider: MediaProviderProtocol?,
|
||||
senderId: String,
|
||||
senderName: String,
|
||||
mediaSource: MediaSourceProxy?,
|
||||
roomId: String) async throws -> UNMutableNotificationContent {
|
||||
guard let mediaProvider, let mediaSource else {
|
||||
return self
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadFileFromSource(mediaSource, fileExtension: "jpg") {
|
||||
case .success(let url):
|
||||
// Initialize only the sender for a one-to-one message intent.
|
||||
let handle = INPersonHandle(value: senderId, type: .unknown)
|
||||
let sender = INPerson(personHandle: handle,
|
||||
nameComponents: nil,
|
||||
displayName: senderName,
|
||||
image: INImage(imageData: try Data(contentsOf: url)),
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil)
|
||||
|
||||
// Because this communication is incoming, you can infer that the current user is
|
||||
// a recipient. Don't include the current user when initializing the intent.
|
||||
let intent = INSendMessageIntent(recipients: nil,
|
||||
outgoingMessageType: .outgoingMessageText,
|
||||
content: nil,
|
||||
speakableGroupName: nil,
|
||||
conversationIdentifier: roomId,
|
||||
serviceName: nil,
|
||||
sender: sender,
|
||||
attachments: nil)
|
||||
|
||||
// Use the intent to initialize the interaction.
|
||||
let interaction = INInteraction(intent: intent, response: nil)
|
||||
|
||||
// Interaction direction is incoming because the user is
|
||||
// receiving this message.
|
||||
interaction.direction = .incoming
|
||||
|
||||
// Donate the interaction before updating notification content.
|
||||
try await interaction.donate()
|
||||
// Update notification content before displaying the
|
||||
// communication notification.
|
||||
let updatedContent = try updating(from: intent)
|
||||
|
||||
return updatedContent.mutableCopy() as! UNMutableNotificationContent
|
||||
case .failure(let error):
|
||||
MXLog.debug("Couldn't add sender icon: \(error)")
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
import UserNotifications
|
||||
|
||||
extension UNNotificationRequest {
|
||||
var roomId: String? {
|
||||
content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String
|
||||
}
|
||||
|
||||
var eventId: String? {
|
||||
content.userInfo[NotificationConstants.UserInfoKey.eventIdentifier] as? String
|
||||
}
|
||||
|
||||
var unreadCount: Int? {
|
||||
content.userInfo[NotificationConstants.UserInfoKey.unreadCount] as? Int
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.usernotifications.service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).NotificationServiceExtension</string>
|
||||
</dict>
|
||||
<key>appGroupIdentifier</key>
|
||||
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||
<key>baseBundleIdentifier</key>
|
||||
<string>$(BASE_BUNDLE_IDENTIFIER)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.usernotifications.filtering</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,89 @@
|
||||
name: NSE
|
||||
|
||||
schemes:
|
||||
NSE:
|
||||
analyze:
|
||||
config: Debug
|
||||
archive:
|
||||
config: Release
|
||||
build:
|
||||
targets:
|
||||
NSE:
|
||||
- running
|
||||
- testing
|
||||
- profiling
|
||||
- analyzing
|
||||
- archiving
|
||||
profile:
|
||||
config: Release
|
||||
run:
|
||||
askForAppToLaunch: true
|
||||
config: Debug
|
||||
debugEnabled: false
|
||||
disableMainThreadChecker: false
|
||||
launchAutomaticallySubstyle: 2
|
||||
test:
|
||||
config: Debug
|
||||
disableMainThreadChecker: false
|
||||
|
||||
targets:
|
||||
NSE:
|
||||
type: app-extension
|
||||
platform: iOS
|
||||
|
||||
dependencies:
|
||||
- package: MatrixRustSDK
|
||||
- package: SwiftyBeaver
|
||||
- package: KeychainAccess
|
||||
- package: Kingfisher
|
||||
|
||||
info:
|
||||
path: ../SupportingFiles/Info.plist
|
||||
properties:
|
||||
CFBundleShortVersionString: $(MARKETING_VERSION)
|
||||
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||
appGroupIdentifier: $(APP_GROUP_IDENTIFIER)
|
||||
baseBundleIdentifier: $(BASE_BUNDLE_IDENTIFIER)
|
||||
NSExtension:
|
||||
NSExtensionPointIdentifier: com.apple.usernotifications.service
|
||||
NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).NotificationServiceExtension
|
||||
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_NAME: NSE
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ${BASE_BUNDLE_IDENTIFIER}.nse
|
||||
MARKETING_VERSION: $(MARKETING_VERSION)
|
||||
CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION)
|
||||
DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM)
|
||||
CODE_SIGN_ENTITLEMENTS: NSE/SupportingFiles/NSE.entitlements
|
||||
SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h
|
||||
debug:
|
||||
release:
|
||||
|
||||
sources:
|
||||
- path: ../Sources
|
||||
- path: ../SupportingFiles
|
||||
- path: ../../ElementX/Sources/Services/Timeline/TimelineItemProxy.swift
|
||||
- path: ../../ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift
|
||||
- path: ../../ElementX/Sources/Services/Keychain/KeychainController.swift
|
||||
- path: ../../ElementX/Sources/Services/UserSession/RestorationToken.swift
|
||||
- path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxyProtocol.swift
|
||||
- path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift
|
||||
- path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift
|
||||
- path: ../../ElementX/Sources/Services/Notification/NotificationConstants.swift
|
||||
- path: ../../ElementX/Sources/Services/Media/MediaProxyProtocol.swift
|
||||
- path: ../../ElementX/Sources/Services/Media/MediaProxy.swift
|
||||
- path: ../../ElementX/Sources/Services/Media/MediaProviderProtocol.swift
|
||||
- path: ../../ElementX/Sources/Services/Media/MediaProvider.swift
|
||||
- path: ../../ElementX/Sources/Services/Media/MediaSourceProxy.swift
|
||||
- path: ../../ElementX/Sources/Services/Background/BackgroundTaskServiceProtocol.swift
|
||||
- path: ../../ElementX/Sources/Services/Background/BackgroundTaskProtocol.swift
|
||||
- path: ../../ElementX/Sources/Services/Cache/FileCache.swift
|
||||
- path: ../../ElementX/Sources/Other/Logging
|
||||
- path: ../../ElementX/Sources/Other/Extensions/Task.swift
|
||||
- path: ../../ElementX/Sources/Other/Extensions/FileManager.swift
|
||||
- path: ../../ElementX/Sources/Other/Extensions/URL.swift
|
||||
- path: ../../ElementX/Sources/Other/Extensions/Bundle.swift
|
||||
- path: ../../ElementX/Sources/Other/Extensions/ImageCache.swift
|
||||
- path: ../../ElementX/Sources/Other/AvatarSize.swift
|
||||
- path: ../../ElementX/Sources/Other/InfoPlistReader.swift
|
||||
Reference in New Issue
Block a user