Notifications (#275)

This commit is contained in:
ismailgulek
2022-11-21 19:37:13 +03:00
committed by GitHub
parent 9f3ed6ca7b
commit d389ce7ad7
74 changed files with 2459 additions and 324 deletions
@@ -92,7 +92,7 @@ class Analytics {
// Catch and log crashes
// MXLogger.logCrashes(true)
// MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString)
// MXLogger.setBuildVersion(Bundle.bundleShortVersionString)
}
/// Use the analytics settings from the supplied user session to configure analytics.
@@ -61,7 +61,7 @@ class BugReportService: BugReportServiceProtocol {
// also enable logging crashes, to send them with bug reports
MXLogger.logCrashes(true)
// set build version for logger
MXLogger.buildVersion = ElementInfoPlist.cfBundleShortVersionString
MXLogger.buildVersion = InfoPlistReader.target.bundleShortVersionString
}
// MARK: - BugReportServiceProtocol
@@ -151,8 +151,8 @@ class BugReportService: BugReportServiceProtocol {
return [
MultipartFormData(key: "user_agent", type: .text(value: "iOS")),
MultipartFormData(key: "app", type: .text(value: applicationId)),
MultipartFormData(key: "version", type: .text(value: ElementInfoPlist.cfBundleShortVersionString)),
MultipartFormData(key: "build", type: .text(value: ElementInfoPlist.cfBundleVersion)),
MultipartFormData(key: "version", type: .text(value: InfoPlistReader.target.bundleShortVersionString)),
MultipartFormData(key: "build", type: .text(value: InfoPlistReader.target.bundleVersion)),
MultipartFormData(key: "os", type: .text(value: os)),
MultipartFormData(key: "resolved_language", type: .text(value: Bundle.preferredLanguages[0])),
MultipartFormData(key: "user_language", type: .text(value: Bundle.elementLanguage ?? "null")),
@@ -32,11 +32,11 @@ class FileCache {
private let fileManager = FileManager.default
private let folder: URL
/// Default instance. Uses `FileCache` as the folder name.
static let `default` = FileCache(folderName: "FileCache")
/// Default instance. Uses `Files` as the folder name.
static let `default` = FileCache(folderName: "Files")
init(folderName: String) {
folder = fileManager.temporaryDirectory.appending(path: folderName, directoryHint: .isDirectory)
folder = URL.cacheBaseDirectory.appending(path: folderName, directoryHint: .isDirectory)
}
// MARK: Private
@@ -44,21 +44,6 @@ class FileCache {
private func filePath(forKey key: String, fileExtension: String) -> URL {
folder.appending(path: key, directoryHint: .notDirectory).appendingPathExtension(fileExtension)
}
private func folderExists() -> Bool {
var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: folder.path(), isDirectory: &isDirectory) else {
return false
}
return isDirectory.boolValue
}
private func createFolderIfNeeded() throws {
guard !folderExists() else {
return
}
try fileManager.createDirectory(at: folder, withIntermediateDirectories: true)
}
}
// MARK: - FileCacheProtocol
@@ -70,7 +55,7 @@ extension FileCache: FileCacheProtocol {
}
func store(_ data: Data, with fileExtension: String, forKey key: String) throws -> URL {
try createFolderIfNeeded()
try fileManager.createDirectoryIfNeeded(at: folder)
let url = filePath(forKey: key, fileExtension: fileExtension)
try data.write(to: url)
return url
@@ -81,7 +66,7 @@ extension FileCache: FileCacheProtocol {
}
func removeAll() throws {
guard folderExists() else {
guard fileManager.directoryExists(at: folder) else {
return
}
try fileManager.removeItem(at: folder)
@@ -52,6 +52,7 @@ class ClientProxy: ClientProxyProtocol {
private let client: ClientProtocol
private let backgroundTaskService: BackgroundTaskServiceProtocol
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
private let mediaProxy: MediaProxyProtocol
private let clientQueue: DispatchQueue
private var slidingSyncObserverToken: StoppableSpawn?
@@ -75,6 +76,8 @@ class ClientProxy: ClientProxyProtocol {
self.backgroundTaskService = backgroundTaskService
clientQueue = .init(label: "ClientProxyQueue",
attributes: .concurrent)
mediaProxy = MediaProxy(client: client,
clientQueue: clientQueue)
await Task.dispatch(on: clientQueue) {
do {
@@ -204,25 +207,7 @@ class ClientProxy: ClientProxyProtocol {
.failure(.failedSettingAccountData)
}
}
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
}
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
try await Task.dispatch(on: clientQueue) {
let bytes = try self.client.getMediaContent(source: source)
return Data(bytes: bytes, count: bytes.count)
}
}
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data {
try await Task.dispatch(on: clientQueue) {
let bytes = try self.client.getMediaThumbnail(source: source, width: UInt64(width), height: UInt64(height))
return Data(bytes: bytes, count: bytes.count)
}
}
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
await Task.dispatch(on: clientQueue) {
do {
@@ -243,6 +228,32 @@ class ClientProxy: ClientProxyProtocol {
}
}
}
// swiftlint:disable:next function_parameter_count
func setPusher(pushkey: String,
kind: PusherKind?,
appId: String,
appDisplayName: String,
deviceDisplayName: String,
profileTag: String?,
lang: String,
url: String?,
format: PushFormat?,
defaultPayload: [AnyHashable: Any]?) async throws {
// let defaultPayloadString = jsonString(from: defaultPayload)
// try await Task.dispatch(on: .global()) {
// try self.client.setPusher(pushkey: pushkey,
// kind: kind?.rustValue,
// appId: appId,
// appDisplayName: appDisplayName,
// deviceDisplayName: deviceDisplayName,
// profileTag: profileTag,
// lang: lang,
// url: url,
// format: format?.rustValue,
// defaultPayload: defaultPayloadString)
// }
}
// MARK: Private
@@ -271,4 +282,29 @@ class ClientProxy: ClientProxyProtocol {
callbacks.send(.receivedSyncUpdate)
}
/// Convenience method to get the json string of an Encodable
private func jsonString(from dictionary: [AnyHashable: Any]?) -> String? {
guard let dictionary,
let data = try? JSONSerialization.data(withJSONObject: dictionary,
options: [.fragmentsAllowed]) else {
return nil
}
return String(data: data, encoding: .utf8)
}
}
extension ClientProxy: MediaProxyProtocol {
func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy {
mediaProxy.mediaSourceForURLString(urlString)
}
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
try await mediaProxy.loadMediaContentForSource(source)
}
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
try await mediaProxy.loadMediaThumbnailForSource(source, width: width, height: height)
}
}
@@ -33,7 +33,32 @@ enum ClientProxyError: Error {
case failedLoadingMedia
}
protocol ClientProxyProtocol {
enum PusherKind {
case http
case email
// var rustValue: MatrixRustSDK.PusherKind {
// switch self {
// case .http:
// return .http
// case .email:
// return .email
// }
// }
}
enum PushFormat {
case eventIdOnly
// var rustValue: MatrixRustSDK.PushFormat {
// switch self {
// case .eventIdOnly:
// return .eventIdOnly
// }
// }
}
protocol ClientProxyProtocol: MediaProxyProtocol {
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
var userIdentifier: String { get }
@@ -62,13 +87,19 @@ protocol ClientProxyProtocol {
func setAccountData<Content: Encodable>(content: Content, type: String) async -> Result<Void, ClientProxyError>
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
func logout() async
// swiftlint:disable:next function_parameter_count
func setPusher(pushkey: String,
kind: PusherKind?,
appId: String,
appDisplayName: String,
deviceDisplayName: String,
profileTag: String?,
lang: String,
url: String?,
format: PushFormat?,
defaultPayload: [AnyHashable: Any]?) async throws
}
@@ -53,15 +53,15 @@ struct MockClientProxy: ClientProxyProtocol {
.failure(.failedSettingAccountData)
}
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy {
.init(urlString: urlString)
}
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
throw ClientProxyError.failedLoadingMedia
}
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data {
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
throw ClientProxyError.failedLoadingMedia
}
@@ -72,4 +72,18 @@ struct MockClientProxy: ClientProxyProtocol {
func logout() async {
// no-op
}
// swiftlint:disable:next function_parameter_count
func setPusher(pushkey: String,
kind: PusherKind?,
appId: String,
appDisplayName: String,
deviceDisplayName: String,
profileTag: String?,
lang: String,
url: String?,
format: PushFormat?,
defaultPayload: [AnyHashable: Any]?) async throws {
// no-op
}
}
@@ -17,13 +17,24 @@
import Foundation
import KeychainAccess
enum KeychainControllerService: String {
case sessions
case tests
var identifier: String {
InfoPlistReader.target.baseBundleIdentifier + "." + rawValue
}
}
class KeychainController: KeychainControllerProtocol {
private let keychain: Keychain
init(identifier: String) {
keychain = Keychain(service: identifier)
init(service: KeychainControllerService,
accessGroup: String) {
keychain = Keychain(service: service.identifier,
accessGroup: accessGroup)
}
func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) {
do {
let tokenData = try JSONEncoder().encode(restorationToken)
@@ -32,7 +43,7 @@ class KeychainController: KeychainControllerProtocol {
MXLog.error("Failed storing user restore token with error: \(error)")
}
}
func restorationTokenForUsername(_ username: String) -> RestorationToken? {
do {
guard let tokenData = try keychain.getData(username) else {
@@ -49,24 +60,24 @@ class KeychainController: KeychainControllerProtocol {
homeserverUrl: legacyRestorationToken.homeURL,
isSoftLogout: legacyRestorationToken.isSoftLogout ?? false))
}
return try JSONDecoder().decode(RestorationToken.self, from: tokenData)
} catch {
MXLog.error("Failed retrieving user restore token")
return nil
}
}
func restorationTokens() -> [KeychainCredentials] {
keychain.allKeys().compactMap { username in
guard let restorationToken = restorationTokenForUsername(username) else {
return nil
}
return KeychainCredentials(userID: username, restorationToken: restorationToken)
}
}
func removeRestorationTokenForUsername(_ username: String) {
do {
try keychain.remove(username)
@@ -74,7 +85,7 @@ class KeychainController: KeychainControllerProtocol {
MXLog.error("Failed removing restore token with error: \(error)")
}
}
func removeAllRestorationTokens() {
do {
try keychain.removeAll()
@@ -18,26 +18,26 @@ import Kingfisher
import UIKit
struct MediaProvider: MediaProviderProtocol {
private let clientProxy: ClientProxyProtocol
private let mediaProxy: MediaProxyProtocol
private let imageCache: Kingfisher.ImageCache
private let fileCache: FileCache
private let backgroundTaskService: BackgroundTaskServiceProtocol
private let backgroundTaskService: BackgroundTaskServiceProtocol?
init(clientProxy: ClientProxyProtocol,
init(mediaProxy: MediaProxyProtocol,
imageCache: Kingfisher.ImageCache,
fileCache: FileCache,
backgroundTaskService: BackgroundTaskServiceProtocol) {
self.clientProxy = clientProxy
backgroundTaskService: BackgroundTaskServiceProtocol?) {
self.mediaProxy = mediaProxy
self.imageCache = imageCache
self.fileCache = fileCache
self.backgroundTaskService = backgroundTaskService
}
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? {
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? {
guard let source else {
return nil
}
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), avatarSize: avatarSize)
let cacheKey = cacheKeyForURLString(source.url, avatarSize: avatarSize)
return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil)
}
@@ -46,19 +46,19 @@ struct MediaProvider: MediaProviderProtocol {
return nil
}
return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), avatarSize: avatarSize)
return imageFromSource(.init(urlString: urlString), avatarSize: avatarSize)
}
func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), avatarSize: avatarSize)
await loadImageFromSource(.init(urlString: urlString), avatarSize: avatarSize)
}
func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
if let image = imageFromSource(source, avatarSize: avatarSize) {
return .success(image)
}
let loadImageBgTask = await backgroundTaskService.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)")
let loadImageBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)")
defer {
loadImageBgTask?.stop()
}
@@ -73,9 +73,9 @@ struct MediaProvider: MediaProviderProtocol {
do {
let imageData: Data
if let avatarSize {
imageData = try await clientProxy.loadMediaThumbnailForSource(source.underlyingSource, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue))
imageData = try await mediaProxy.loadMediaThumbnailForSource(source, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue))
} else {
imageData = try await clientProxy.loadMediaContentForSource(source.underlyingSource)
imageData = try await mediaProxy.loadMediaContentForSource(source)
}
guard let image = UIImage(data: imageData) else {
@@ -92,20 +92,20 @@ struct MediaProvider: MediaProviderProtocol {
}
}
func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL? {
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL? {
guard let source else {
return nil
}
let cacheKey = fileCacheKeyForURLString(source.underlyingSource.url())
let cacheKey = fileCacheKeyForURLString(source.url)
return fileCache.file(forKey: cacheKey, fileExtension: fileExtension)
}
@discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result<URL, MediaProviderError> {
@discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result<URL, MediaProviderError> {
if let url = fileFromSource(source, fileExtension: fileExtension) {
return .success(url)
}
let loadFileBgTask = await backgroundTaskService.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)")
let loadFileBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)")
defer {
loadFileBgTask?.stop()
}
@@ -113,7 +113,7 @@ struct MediaProvider: MediaProviderProtocol {
let cacheKey = fileCacheKeyForURLString(source.url)
do {
let data = try await clientProxy.loadMediaContentForSource(source.underlyingSource)
let data = try await mediaProxy.loadMediaContentForSource(source)
let url = try fileCache.store(data, with: fileExtension, forKey: cacheKey)
return .success(url)
@@ -128,12 +128,12 @@ struct MediaProvider: MediaProviderProtocol {
return nil
}
return fileFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)),
return fileFromSource(MediaSourceProxy(urlString: urlString),
fileExtension: fileExtension)
}
func loadFileFromURLString(_ urlString: String, fileExtension: String) async -> Result<URL, MediaProviderError> {
await loadFileFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)),
await loadFileFromSource(MediaSourceProxy(urlString: urlString),
fileExtension: fileExtension)
}
@@ -24,17 +24,17 @@ enum MediaProviderError: Error {
}
protocol MediaProviderProtocol {
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage?
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage?
@discardableResult func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
func imageFromURLString(_ urlString: String?, avatarSize: AvatarSize?) -> UIImage?
@discardableResult func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL?
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL?
@discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result<URL, MediaProviderError>
@discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result<URL, MediaProviderError>
func fileFromURLString(_ urlString: String?, fileExtension: String) -> URL?
@@ -42,11 +42,11 @@ protocol MediaProviderProtocol {
}
extension MediaProviderProtocol {
func imageFromSource(_ source: MediaSource?) -> UIImage? {
func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? {
imageFromSource(source, avatarSize: nil)
}
@discardableResult func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy) async -> Result<UIImage, MediaProviderError> {
await loadImageFromSource(source, avatarSize: nil)
}
@@ -0,0 +1,49 @@
//
// 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 Combine
import Foundation
import MatrixRustSDK
import UIKit
class MediaProxy: MediaProxyProtocol {
private let client: ClientProtocol
private let clientQueue: DispatchQueue
init(client: ClientProtocol,
clientQueue: DispatchQueue = .global()) {
self.client = client
self.clientQueue = clientQueue
}
func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy {
.init(urlString: urlString)
}
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
try await Task.dispatch(on: clientQueue) {
let bytes = try self.client.getMediaContent(source: source.underlyingSource)
return Data(bytes: bytes, count: bytes.count)
}
}
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
try await Task.dispatch(on: clientQueue) {
let bytes = try self.client.getMediaThumbnail(source: source.underlyingSource, width: UInt64(width), height: UInt64(height))
return Data(bytes: bytes, count: bytes.count)
}
}
}
@@ -0,0 +1,25 @@
//
// 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
protocol MediaProxyProtocol {
func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data
}
@@ -17,24 +17,24 @@
import Foundation
import MatrixRustSDK
struct MediaSource: Equatable {
let underlyingSource: MatrixRustSDK.MediaSource
struct MediaSourceProxy: Equatable {
let underlyingSource: MediaSource
init(source: MediaSource) {
underlyingSource = source
}
init(urlString: String) {
underlyingSource = mediaSourceFromUrl(url: urlString)
}
var url: String {
underlyingSource.url()
}
init(source: MatrixRustSDK.MediaSource) {
underlyingSource = source
}
init(urlString: String) {
underlyingSource = MatrixRustSDK.mediaSourceFromUrl(url: urlString)
}
// MARK: - Equatable
static func == (lhs: MediaSource, rhs: MediaSource) -> Bool {
lhs.underlyingSource.url() == rhs.underlyingSource.url()
static func == (lhs: MediaSourceProxy, rhs: MediaSourceProxy) -> Bool {
lhs.url == rhs.url
}
}
@@ -18,11 +18,11 @@ import Foundation
import UIKit
struct MockMediaProvider: MediaProviderProtocol {
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? {
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? {
nil
}
func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
.failure(.failedRetrievingImage)
}
@@ -50,11 +50,11 @@ struct MockMediaProvider: MediaProviderProtocol {
return .success(image)
}
func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL? {
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL? {
nil
}
@discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result<URL, MediaProviderError> {
@discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result<URL, MediaProviderError> {
.failure(.failedRetrievingFile)
}
@@ -0,0 +1,37 @@
//
// 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
class MockNotificationManager: NotificationManagerProtocol {
// MARK: NotificationManagerProtocol
var isAvailable: Bool {
false
}
weak var delegate: NotificationManagerDelegate?
func start() {
delegate?.authorizationStatusUpdated(self, granted: false)
}
func register(with deviceToken: Data) { }
func registrationFailed(with error: Error) { }
func showLocalNotification(with title: String, subtitle: String?) { }
}
@@ -0,0 +1,166 @@
//
// 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 UIKit
import UserNotifications
class NotificationManager: NSObject, NotificationManagerProtocol {
private let notificationCenter = UNUserNotificationCenter.current()
private let clientProxy: ClientProxyProtocol
init(clientProxy: ClientProxyProtocol) {
self.clientProxy = clientProxy
super.init()
}
// MARK: NotificationManagerProtocol
weak var delegate: NotificationManagerDelegate?
var isAvailable: Bool {
true
}
func start() {
let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply,
title: ElementL10n.actionQuickReply,
options: [])
let replyCategory = UNNotificationCategory(identifier: NotificationConstants.Category.reply,
actions: [replyAction],
intentIdentifiers: [],
options: [])
notificationCenter.setNotificationCategories([replyCategory])
notificationCenter.delegate = self
Task {
do {
let granted = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge])
MXLog.debug("[NotificationManager] permission granted: \(granted)")
await MainActor.run {
delegate?.authorizationStatusUpdated(self, granted: granted)
}
} catch {
MXLog.debug("[NotificationManager] request authorization failed: \(error)")
}
}
}
func register(with deviceToken: Data) {
setPusher(with: deviceToken, clientProxy: clientProxy)
}
func registrationFailed(with error: Error) { }
func showLocalNotification(with title: String, subtitle: String?) {
let content = UNMutableNotificationContent()
content.title = title
if let subtitle {
content.subtitle = subtitle
}
let request = UNNotificationRequest(identifier: ProcessInfo.processInfo.globallyUniqueString,
content: content,
trigger: nil)
Task {
do {
try await notificationCenter.add(request)
MXLog.debug("[NotificationManager] show local notification succeeded")
} catch {
MXLog.debug("[NotificationManager] show local notification failed: \(error)")
}
}
}
private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) {
Task {
do {
try await clientProxy.setPusher(pushkey: deviceToken.base64EncodedString(),
kind: .http,
appId: BuildSettings.pusherAppId,
appDisplayName: "\(InfoPlistReader.target.bundleDisplayName) (iOS)",
deviceDisplayName: UIDevice.current.name,
profileTag: pusherProfileTag(),
lang: Bundle.preferredLanguages.first ?? "en",
url: BuildSettings.pushGatewayBaseURL.absoluteString,
format: .eventIdOnly,
defaultPayload: [
"aps": [
"mutable-content": 1,
"alert": [
"loc-key": "Notification",
"loc-args": []
]
]
])
MXLog.debug("[NotificationManager] set pusher succeeded")
} catch {
MXLog.debug("[NotificationManager] set pusher failed: \(error)")
}
}
}
private func pusherProfileTag() -> String {
if let currentTag = ElementSettings.shared.pusherProfileTag {
return currentTag
}
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
let newTag = (0..<16).map { _ in
let offset = Int.random(in: 0..<chars.count)
return String(chars[chars.index(chars.startIndex, offsetBy: offset)])
}.joined()
ElementSettings.shared.pusherProfileTag = newTag
return newTag
}
}
// MARK: - UNUserNotificationCenterDelegate
extension NotificationManager: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
guard ElementSettings.shared.enableInAppNotifications else {
return []
}
guard let delegate else {
return [.badge, .sound, .list, .banner]
}
guard delegate.shouldDisplayInAppNotification(self, content: notification.request.content) else {
return []
}
return [.badge, .sound, .list, .banner]
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
switch response.actionIdentifier {
case NotificationConstants.Action.inlineReply:
guard let response = response as? UNTextInputNotificationResponse else {
return
}
await delegate?.handleInlineReply(self,
content: response.notification.request.content,
replyText: response.userText)
case UNNotificationDefaultActionIdentifier:
await delegate?.notificationTapped(self,
content: response.notification.request.content)
default:
break
}
}
}
@@ -0,0 +1,42 @@
//
// 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
protocol NotificationManagerDelegate: AnyObject {
func authorizationStatusUpdated(_ service: NotificationManagerProtocol,
granted: Bool)
func shouldDisplayInAppNotification(_ service: NotificationManagerProtocol,
content: UNNotificationContent) -> Bool
func notificationTapped(_ service: NotificationManagerProtocol,
content: UNNotificationContent) async
func handleInlineReply(_ service: NotificationManagerProtocol,
content: UNNotificationContent,
replyText: String) async
}
// MARK: - NotificationManagerProtocol
protocol NotificationManagerProtocol {
var isAvailable: Bool { get }
var delegate: NotificationManagerDelegate? { get set }
func start()
func register(with deviceToken: Data)
func registrationFailed(with error: Error)
func showLocalNotification(with title: String, subtitle: String?)
}
@@ -0,0 +1,34 @@
//
// 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
enum NotificationConstants {
enum UserInfoKey {
static let roomIdentifier = "room_id"
static let eventIdentifier = "event_id"
static let unreadCount = "unread_count"
}
enum Category {
static let discard = "discard"
static let reply = "reply"
}
enum Action {
static let inlineReply = "inline-reply"
}
}
@@ -16,9 +16,8 @@
import Foundation
extension FileManager {
/// The URL of the primary app group container.
@objc var appGroupContainerURL: URL? {
containerURL(forSecurityApplicationGroupIdentifier: ElementInfoPlist.appGroupIdentifier)
class MockNotificationServiceProxy: NotificationServiceProxyProtocol {
func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy? {
nil
}
}
@@ -0,0 +1,73 @@
//
// 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
struct NotificationItemProxy {
// let notificationItem: NotificationItem
//
// init(notificationItem: NotificationItem) {
// self.notificationItem = notificationItem
// }
//
// var timelineItemProxy: TimelineItemProxy {
// .init(item: notificationItem.item)
// }
//
// var title: String {
// notificationItem.title
// }
//
// var subtitle: String? {
// notificationItem.subtitle
// }
//
// var isNoisy: Bool {
// notificationItem.isNoisy
// }
//
// var avatarUrl: String? {
// notificationItem.avatarUrl
// }
//
// var avatarMediaSource: MediaSourceProxy? {
// guard let avatarUrl else {
// return nil
// }
// return .init(urlString: avatarUrl)
// }
var title: String {
"Title"
}
var subtitle: String? {
nil
}
var isNoisy: Bool {
true
}
var avatarUrl: String? {
nil
}
var avatarMediaSource: MediaSourceProxy? {
nil
}
}
@@ -0,0 +1,37 @@
//
// 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 NotificationServiceProxy: NotificationServiceProxyProtocol {
// private let service: NotificationServiceProtocol
init(basePath: String,
userId: String) {
// service = NotificationService(basePath: basePath, userId: userId)
}
func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy? {
nil
// try await Task.dispatch(on: .global()) {
// guard let item = try self.service.getNotificationItem(roomId: roomId, eventId: eventId) else {
// return nil
// }
// return .init(notificationItem: item)
// }
}
}
@@ -0,0 +1,21 @@
//
// 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
protocol NotificationServiceProxyProtocol {
func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy?
}
@@ -113,8 +113,8 @@ extension MatrixRustSDK.ImageMessageContent: MessageContentProtocol { }
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.image`.
extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent {
var source: MediaSource {
MediaSource(source: content.source)
var source: MediaSourceProxy {
.init(source: content.source)
}
var width: CGFloat? {
@@ -134,15 +134,15 @@ extension MatrixRustSDK.VideoMessageContent: MessageContentProtocol { }
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.video`.
extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent {
var source: MediaSource {
MediaSource(source: content.source)
var source: MediaSourceProxy {
.init(source: content.source)
}
var thumbnailSource: MediaSource? {
var thumbnailSource: MediaSourceProxy? {
guard let src = content.info?.thumbnailSource else {
return nil
}
return MediaSource(source: src)
return .init(source: src)
}
var duration: UInt64 {
@@ -166,14 +166,14 @@ extension MatrixRustSDK.FileMessageContent: MessageContentProtocol { }
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.file`.
extension MessageTimelineItem where Content == MatrixRustSDK.FileMessageContent {
var source: MediaSource {
MediaSource(source: content.source)
var source: MediaSourceProxy {
.init(source: content.source)
}
var thumbnailSource: MediaSource? {
var thumbnailSource: MediaSourceProxy? {
guard let src = content.info?.thumbnailSource else {
return nil
}
return MediaSource(source: src)
return .init(source: src)
}
}
@@ -29,8 +29,8 @@ struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat
var senderDisplayName: String?
var senderAvatar: UIImage?
let source: MediaSource?
let thumbnailSource: MediaSource?
let source: MediaSourceProxy?
let thumbnailSource: MediaSourceProxy?
var cachedFileURL: URL?
var properties = RoomTimelineItemProperties()
@@ -29,7 +29,7 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
var senderDisplayName: String?
var senderAvatar: UIImage?
let source: MediaSource?
let source: MediaSourceProxy?
var image: UIImage?
var width: CGFloat?
@@ -30,8 +30,8 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
var senderAvatar: UIImage?
let duration: UInt64
let source: MediaSource?
let thumbnailSource: MediaSource?
let source: MediaSourceProxy?
let thumbnailSource: MediaSourceProxy?
var image: UIImage?
var cachedVideoURL: URL?
@@ -42,7 +42,17 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
func start() {
stateMachine.processEvent(.start)
}
func stop() { }
func isDisplayingRoomScreen(withRoomId roomId: String) -> Bool {
stateMachine.isDisplayingRoomScreen(withRoomId: roomId)
}
func tryDisplayingRoomScreen(roomId: String) {
stateMachine.processEvent(.showRoomScreen(roomId: roomId))
}
// MARK: - Private
// swiftlint:disable:next cyclomatic_complexity
@@ -150,4 +150,14 @@ class UserSessionFlowCoordinatorStateMachine {
func addErrorHandler(_ handler: @escaping StateMachine<State, Event>.Handler) {
stateMachine.addErrorHandler(handler: handler)
}
/// Flag indicating the machine is displaying room screen with given room identifier
func isDisplayingRoomScreen(withRoomId roomId: String) -> Bool {
switch stateMachine.state {
case .roomScreen(let displayedRoomId):
return roomId == displayedRoomId
default:
return false
}
}
}
@@ -26,26 +26,14 @@ class UserSessionStore: UserSessionStoreProtocol {
var hasSessions: Bool { !keychainController.restorationTokens().isEmpty }
/// The base directory where all session data is stored.
private(set) lazy var baseDirectory: URL = {
guard let appGroupContainerURL = FileManager.default.appGroupContainerURL else {
fatalError("Should always be able to retrieve the container directory")
}
let url = appGroupContainerURL
.appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Caches", isDirectory: true)
.appendingPathComponent("Sessions", isDirectory: true)
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil)
MXLog.debug("Setup base directory at: \(url)")
return url
}()
let baseDirectory: URL
init(bundleIdentifier: String, backgroundTaskService: BackgroundTaskServiceProtocol) {
keychainController = KeychainController(identifier: bundleIdentifier)
init(backgroundTaskService: BackgroundTaskServiceProtocol) {
keychainController = KeychainController(service: .sessions,
accessGroup: InfoPlistReader.target.appGroupIdentifier)
self.backgroundTaskService = backgroundTaskService
baseDirectory = .sessionsBaseDirectory
MXLog.debug("Setup base directory at: \(baseDirectory)")
}
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError> {
@@ -58,7 +46,7 @@ class UserSessionStore: UserSessionStoreProtocol {
switch await restorePreviousLogin(credentials) {
case .success(let clientProxy):
return .success(UserSession(clientProxy: clientProxy,
mediaProvider: MediaProvider(clientProxy: clientProxy,
mediaProvider: MediaProvider(mediaProxy: clientProxy,
imageCache: .onlyInMemory,
fileCache: .default,
backgroundTaskService: backgroundTaskService)))
@@ -77,7 +65,7 @@ class UserSessionStore: UserSessionStoreProtocol {
switch await setupProxyForClient(client) {
case .success(let clientProxy):
return .success(UserSession(clientProxy: clientProxy,
mediaProvider: MediaProvider(clientProxy: clientProxy,
mediaProvider: MediaProvider(mediaProxy: clientProxy,
imageCache: .onlyInMemory,
fileCache: .default,
backgroundTaskService: backgroundTaskService)))