mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 13:37:11 +00:00
Notifications (#275)
This commit is contained in:
parent
9f3ed6ca7b
commit
d389ce7ad7
File diff suppressed because it is too large
Load Diff
98
ElementX.xcodeproj/xcshareddata/xcschemes/NSE.xcscheme
Normal file
98
ElementX.xcodeproj/xcshareddata/xcschemes/NSE.xcscheme
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1200"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
runPostActionsOnFailure = "NO">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "FEB53A5BC378C913769656D8"
|
||||||
|
BuildableName = "NSE.appex"
|
||||||
|
BlueprintName = "NSE"
|
||||||
|
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
onlyGenerateCoverageForSpecifiedTargets = "NO">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "FEB53A5BC378C913769656D8"
|
||||||
|
BuildableName = "NSE.appex"
|
||||||
|
BlueprintName = "NSE"
|
||||||
|
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "YES"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "FEB53A5BC378C913769656D8"
|
||||||
|
BuildableName = "NSE.appex"
|
||||||
|
BlueprintName = "NSE"
|
||||||
|
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "FEB53A5BC378C913769656D8"
|
||||||
|
BuildableName = "NSE.appex"
|
||||||
|
BlueprintName = "NSE"
|
||||||
|
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -31,3 +31,5 @@
|
|||||||
|
|
||||||
// Parameter is the application display name (e.g. "ElementX")
|
// Parameter is the application display name (e.g. "ElementX")
|
||||||
"default_session_display_name" = "%@ iOS";
|
"default_session_display_name" = "%@ iOS";
|
||||||
|
|
||||||
|
"Notification" = "Notification";
|
||||||
|
BIN
ElementX/Resources/Sounds/message.caf
Normal file
BIN
ElementX/Resources/Sounds/message.caf
Normal file
Binary file not shown.
@ -31,7 +31,7 @@ struct ServiceLocator {
|
|||||||
let userNotificationController: UserNotificationControllerProtocol
|
let userNotificationController: UserNotificationControllerProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppCoordinator: CoordinatorProtocol {
|
class AppCoordinator: AppCoordinatorProtocol {
|
||||||
private let stateMachine: AppCoordinatorStateMachine
|
private let stateMachine: AppCoordinatorStateMachine
|
||||||
private let navigationController: NavigationController
|
private let navigationController: NavigationController
|
||||||
private let userSessionStore: UserSessionStoreProtocol
|
private let userSessionStore: UserSessionStoreProtocol
|
||||||
@ -40,6 +40,7 @@ class AppCoordinator: CoordinatorProtocol {
|
|||||||
didSet {
|
didSet {
|
||||||
deobserveUserSessionChanges()
|
deobserveUserSessionChanges()
|
||||||
if let userSession, !userSession.isSoftLogout {
|
if let userSession, !userSession.isSoftLogout {
|
||||||
|
configureNotificationManager()
|
||||||
observeUserSessionChanges()
|
observeUserSessionChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,7 +53,8 @@ class AppCoordinator: CoordinatorProtocol {
|
|||||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private(set) var notificationManager: NotificationManagerProtocol?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
navigationController = NavigationController()
|
navigationController = NavigationController()
|
||||||
stateMachine = AppCoordinatorStateMachine()
|
stateMachine = AppCoordinatorStateMachine()
|
||||||
@ -60,17 +62,12 @@ class AppCoordinator: CoordinatorProtocol {
|
|||||||
bugReportService = BugReportService(withBaseURL: BuildSettings.bugReportServiceBaseURL, sentryURL: BuildSettings.bugReportSentryURL)
|
bugReportService = BugReportService(withBaseURL: BuildSettings.bugReportServiceBaseURL, sentryURL: BuildSettings.bugReportSentryURL)
|
||||||
|
|
||||||
navigationController.setRootCoordinator(SplashScreenCoordinator())
|
navigationController.setRootCoordinator(SplashScreenCoordinator())
|
||||||
|
|
||||||
ServiceLocator.serviceLocator = ServiceLocator(userNotificationController: UserNotificationController(rootCoordinator: navigationController))
|
ServiceLocator.serviceLocator = ServiceLocator(userNotificationController: UserNotificationController(rootCoordinator: navigationController))
|
||||||
|
|
||||||
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
|
|
||||||
fatalError("Should have a valid bundle identifier at this point")
|
|
||||||
}
|
|
||||||
|
|
||||||
backgroundTaskService = UIKitBackgroundTaskService(withApplication: UIApplication.shared)
|
backgroundTaskService = UIKitBackgroundTaskService(withApplication: UIApplication.shared)
|
||||||
|
|
||||||
userSessionStore = UserSessionStore(bundleIdentifier: bundleIdentifier,
|
userSessionStore = UserSessionStore(backgroundTaskService: backgroundTaskService)
|
||||||
backgroundTaskService: backgroundTaskService)
|
|
||||||
|
|
||||||
setupStateMachine()
|
setupStateMachine()
|
||||||
|
|
||||||
@ -97,6 +94,7 @@ class AppCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
private func setupLogging() {
|
private func setupLogging() {
|
||||||
let loggerConfiguration = MXLogConfiguration()
|
let loggerConfiguration = MXLogConfiguration()
|
||||||
|
loggerConfiguration.maxLogFilesCount = 10
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// This exposes the full Rust side tracing subscriber filter for more flexibility.
|
// This exposes the full Rust side tracing subscriber filter for more flexibility.
|
||||||
@ -252,6 +250,8 @@ class AppCoordinator: CoordinatorProtocol {
|
|||||||
// regardless of the result, clear user data
|
// regardless of the result, clear user data
|
||||||
userSessionStore.logout(userSession: userSession)
|
userSessionStore.logout(userSession: userSession)
|
||||||
userSession = nil
|
userSession = nil
|
||||||
|
notificationManager?.delegate = nil
|
||||||
|
notificationManager = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,6 +268,38 @@ class AppCoordinator: CoordinatorProtocol {
|
|||||||
startAuthentication()
|
startAuthentication()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func configureNotificationManager() {
|
||||||
|
guard BuildSettings.enableNotifications else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard notificationManager == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let manager = NotificationManager(clientProxy: userSession.clientProxy)
|
||||||
|
if manager.isAvailable {
|
||||||
|
manager.delegate = self
|
||||||
|
notificationManager = manager
|
||||||
|
manager.start()
|
||||||
|
|
||||||
|
if let appDelegate = AppDelegate.shared {
|
||||||
|
appDelegate.callbacks
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] callback in
|
||||||
|
switch callback {
|
||||||
|
case .registeredNotifications(let deviceToken):
|
||||||
|
self?.notificationManager?.register(with: deviceToken)
|
||||||
|
case .failedToRegisteredNotifications(let error):
|
||||||
|
self?.notificationManager?.registrationFailed(with: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
} else {
|
||||||
|
MXLog.debug("Couldn't register to AppDelegate callbacks")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func observeUserSessionChanges() {
|
private func observeUserSessionChanges() {
|
||||||
userSession.callbacks
|
userSession.callbacks
|
||||||
@ -321,3 +353,51 @@ extension AppCoordinator: AuthenticationCoordinatorDelegate {
|
|||||||
stateMachine.processEvent(.succeededSigningIn)
|
stateMachine.processEvent(.succeededSigningIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - NotificationManagerDelegate
|
||||||
|
|
||||||
|
extension AppCoordinator: NotificationManagerDelegate {
|
||||||
|
func authorizationStatusUpdated(_ service: NotificationManagerProtocol, granted: Bool) {
|
||||||
|
if granted {
|
||||||
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldDisplayInAppNotification(_ service: NotificationManagerProtocol, content: UNNotificationContent) -> Bool {
|
||||||
|
guard let roomId = content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
guard let userSessionFlowCoordinator else {
|
||||||
|
// there is not a user session yet
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !userSessionFlowCoordinator.isDisplayingRoomScreen(withRoomId: roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func notificationTapped(_ service: NotificationManagerProtocol, content: UNNotificationContent) async {
|
||||||
|
MXLog.debug("[AppCoordinator] tappedNotification")
|
||||||
|
|
||||||
|
guard let roomId = content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSessionFlowCoordinator?.tryDisplayingRoomScreen(roomId: roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async {
|
||||||
|
MXLog.debug("[AppCoordinator] handle notification reply")
|
||||||
|
|
||||||
|
guard let roomId = content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let roomProxy = await userSession.clientProxy.roomForIdentifier(roomId)
|
||||||
|
switch await roomProxy?.sendMessage(replyText) {
|
||||||
|
case .success:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// error or no room proxy
|
||||||
|
service.showLocalNotification(with: "⚠️ " + ElementL10n.dialogTitleError,
|
||||||
|
subtitle: ElementL10n.a11yErrorSomeMessageNotSent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
21
ElementX/Sources/Application/AppCoordinatorProtocol.swift
Normal file
21
ElementX/Sources/Application/AppCoordinatorProtocol.swift
Normal file
@ -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 AppCoordinatorProtocol: CoordinatorProtocol {
|
||||||
|
var notificationManager: NotificationManagerProtocol? { get }
|
||||||
|
}
|
@ -14,38 +14,30 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
@main
|
enum AppDelegateCallback {
|
||||||
struct Application: App {
|
case registeredNotifications(deviceToken: Data)
|
||||||
@UIApplicationDelegateAdaptor(AppDelegate.self) private var applicationDelegate
|
case failedToRegisteredNotifications(error: Error)
|
||||||
private let applicationCoordinator: CoordinatorProtocol
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if Tests.isRunningUITests {
|
|
||||||
applicationCoordinator = UITestsAppCoordinator()
|
|
||||||
} else {
|
|
||||||
applicationCoordinator = AppCoordinator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
if Tests.isRunningUnitTests {
|
|
||||||
EmptyView()
|
|
||||||
} else {
|
|
||||||
applicationCoordinator.toPresentable()
|
|
||||||
.tint(.element.accent)
|
|
||||||
.task {
|
|
||||||
applicationCoordinator.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
private(set) static var shared: AppDelegate!
|
||||||
true
|
let callbacks = PassthroughSubject<AppDelegateCallback, Never>()
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||||
|
// worst singleton ever
|
||||||
|
Self.shared = self
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||||
|
callbacks.send(.registeredNotifications(deviceToken: deviceToken))
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||||
|
callbacks.send(.failedToRegisteredNotifications(error: error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
45
ElementX/Sources/Application/Application.swift
Normal file
45
ElementX/Sources/Application/Application.swift
Normal file
@ -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 SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct Application: App {
|
||||||
|
@UIApplicationDelegateAdaptor(AppDelegate.self) private var applicationDelegate
|
||||||
|
private let applicationCoordinator: AppCoordinatorProtocol
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if Tests.isRunningUITests {
|
||||||
|
applicationCoordinator = UITestsAppCoordinator()
|
||||||
|
} else {
|
||||||
|
applicationCoordinator = AppCoordinator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
if Tests.isRunningUnitTests {
|
||||||
|
EmptyView()
|
||||||
|
} else {
|
||||||
|
applicationCoordinator.toPresentable()
|
||||||
|
.tint(.element.accent)
|
||||||
|
.task {
|
||||||
|
applicationCoordinator.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,12 +17,22 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
final class BuildSettings {
|
final class BuildSettings {
|
||||||
|
// MARK: - Bundle Settings
|
||||||
|
|
||||||
|
static var pusherAppId: String {
|
||||||
|
#if DEBUG
|
||||||
|
InfoPlistReader.target.baseBundleIdentifier + ".ios.dev"
|
||||||
|
#else
|
||||||
|
InfoPlistReader.target.baseBundleIdentifier + ".ios.prod"
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Servers
|
// MARK: - Servers
|
||||||
|
|
||||||
static let defaultHomeserverAddress = "matrix.org"
|
static let defaultHomeserverAddress = "matrix.org"
|
||||||
|
|
||||||
static let defaultSlidingSyncProxyBaseURLString = "https://slidingsync.lab.element.dev"
|
static let defaultSlidingSyncProxyBaseURLString = "https://slidingsync.lab.element.dev"
|
||||||
|
static let pushGatewayBaseURL = URL(staticString: "https://matrix.org/_matrix/push/v1/notify")
|
||||||
|
|
||||||
// MARK: - Bug report
|
// MARK: - Bug report
|
||||||
|
|
||||||
static let bugReportServiceBaseURL = URL(staticString: "https://riot.im/bugreports")
|
static let bugReportServiceBaseURL = URL(staticString: "https://riot.im/bugreports")
|
||||||
@ -38,14 +48,14 @@ final class BuildSettings {
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
/// The configuration to use for analytics during development. Set `isEnabled` to false to disable analytics in debug builds.
|
/// The configuration to use for analytics during development. Set `isEnabled` to false to disable analytics in debug builds.
|
||||||
/// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations.
|
/// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations.
|
||||||
static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: ElementInfoPlist.cfBundleIdentifier.starts(with: "io.element.elementx"),
|
static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: InfoPlistReader.target.bundleIdentifier.starts(with: "io.element.elementx"),
|
||||||
host: "https://posthog.element.dev",
|
host: "https://posthog.element.dev",
|
||||||
apiKey: "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN",
|
apiKey: "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN",
|
||||||
termsURL: URL(staticString: "https://element.io/cookie-policy"))
|
termsURL: URL(staticString: "https://element.io/cookie-policy"))
|
||||||
#else
|
#else
|
||||||
/// The configuration to use for analytics. Set `isEnabled` to false to disable analytics.
|
/// The configuration to use for analytics. Set `isEnabled` to false to disable analytics.
|
||||||
/// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations.
|
/// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations.
|
||||||
static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: ElementInfoPlist.cfBundleIdentifier.starts(with: "io.element.elementx"),
|
static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: InfoPlistReader.target.bundleIdentifier.starts(with: "io.element.elementx"),
|
||||||
host: "https://posthog.hss.element.io",
|
host: "https://posthog.hss.element.io",
|
||||||
apiKey: "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
|
apiKey: "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
|
||||||
termsURL: URL(staticString: "https://element.io/cookie-policy"))
|
termsURL: URL(staticString: "https://element.io/cookie-policy"))
|
||||||
@ -63,4 +73,8 @@ final class BuildSettings {
|
|||||||
// MARK: - Other
|
// MARK: - Other
|
||||||
|
|
||||||
static var permalinkBaseURL = URL(staticString: "https://matrix.to")
|
static var permalinkBaseURL = URL(staticString: "https://matrix.to")
|
||||||
|
|
||||||
|
// MARK: - Notifications
|
||||||
|
|
||||||
|
static let enableNotifications = false
|
||||||
}
|
}
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
// swiftlint:disable all
|
|
||||||
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
// swiftlint:disable superfluous_disable_command
|
|
||||||
// swiftlint:disable file_length
|
|
||||||
|
|
||||||
// MARK: - Plist Files
|
|
||||||
|
|
||||||
// swiftlint:disable identifier_name line_length type_body_length
|
|
||||||
internal enum ElementInfoPlist {
|
|
||||||
private static let _document = PlistDocument(path: "Info.plist")
|
|
||||||
|
|
||||||
internal static let cfBundleDevelopmentRegion: String = _document["CFBundleDevelopmentRegion"]
|
|
||||||
internal static let cfBundleDisplayName: String = _document["CFBundleDisplayName"]
|
|
||||||
internal static let cfBundleExecutable: String = _document["CFBundleExecutable"]
|
|
||||||
internal static let cfBundleIdentifier: String = _document["CFBundleIdentifier"]
|
|
||||||
internal static let cfBundleInfoDictionaryVersion: String = _document["CFBundleInfoDictionaryVersion"]
|
|
||||||
internal static let cfBundleName: String = _document["CFBundleName"]
|
|
||||||
internal static let cfBundlePackageType: String = _document["CFBundlePackageType"]
|
|
||||||
internal static let cfBundleShortVersionString: String = _document["CFBundleShortVersionString"]
|
|
||||||
internal static let cfBundleVersion: String = _document["CFBundleVersion"]
|
|
||||||
internal static let itsAppUsesNonExemptEncryption: Bool = _document["ITSAppUsesNonExemptEncryption"]
|
|
||||||
internal static let uiLaunchStoryboardName: String = _document["UILaunchStoryboardName"]
|
|
||||||
internal static let uiSupportedInterfaceOrientations: [String] = _document["UISupportedInterfaceOrientations"]
|
|
||||||
internal static let appGroupIdentifier: String = _document["appGroupIdentifier"]
|
|
||||||
}
|
|
||||||
// swiftlint:enable identifier_name line_length type_body_length
|
|
||||||
|
|
||||||
// MARK: - Implementation Details
|
|
||||||
|
|
||||||
private func arrayFromPlist<T>(at path: String) -> [T] {
|
|
||||||
guard let url = BundleToken.bundle.url(forResource: path, withExtension: nil),
|
|
||||||
let data = NSArray(contentsOf: url) as? [T] else {
|
|
||||||
fatalError("Unable to load PLIST at path: \(path)")
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct PlistDocument {
|
|
||||||
let data: [String: Any]
|
|
||||||
|
|
||||||
init(path: String) {
|
|
||||||
self.data = BundleToken.bundle.infoDictionary ?? [:]
|
|
||||||
}
|
|
||||||
|
|
||||||
subscript<T>(key: String) -> T {
|
|
||||||
guard let result = data[key] as? T else {
|
|
||||||
fatalError("Property '\(key)' is not of type \(T.self)")
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// swiftlint:disable convenience_type
|
|
||||||
private final class BundleToken {
|
|
||||||
static let bundle: Bundle = {
|
|
||||||
#if SWIFT_PACKAGE
|
|
||||||
return Bundle.module
|
|
||||||
#else
|
|
||||||
return Bundle(for: BundleToken.self)
|
|
||||||
#endif
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
// swiftlint:enable convenience_type
|
|
@ -24,6 +24,8 @@ extension ElementL10n {
|
|||||||
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
|
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
|
||||||
/// Tablet
|
/// Tablet
|
||||||
public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device")
|
public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device")
|
||||||
|
/// Notification
|
||||||
|
public static let notification = ElementL10n.tr("Untranslated", "Notification")
|
||||||
/// Editing
|
/// Editing
|
||||||
public static let roomTimelineEditing = ElementL10n.tr("Untranslated", "room_timeline_editing")
|
public static let roomTimelineEditing = ElementL10n.tr("Untranslated", "room_timeline_editing")
|
||||||
/// Failed creating the permalink
|
/// Failed creating the permalink
|
||||||
|
@ -26,13 +26,15 @@ final class ElementSettings: ObservableObject {
|
|||||||
case enableAnalytics
|
case enableAnalytics
|
||||||
case isIdentifiedForAnalytics
|
case isIdentifiedForAnalytics
|
||||||
case slidingSyncProxyBaseURLString
|
case slidingSyncProxyBaseURLString
|
||||||
|
case enableInAppNotifications
|
||||||
|
case pusherProfileTag
|
||||||
}
|
}
|
||||||
|
|
||||||
static let shared = ElementSettings()
|
static let shared = ElementSettings()
|
||||||
|
|
||||||
/// UserDefaults to be used on reads and writes.
|
/// UserDefaults to be used on reads and writes.
|
||||||
static var store: UserDefaults {
|
static var store: UserDefaults {
|
||||||
guard let userDefaults = UserDefaults(suiteName: ElementInfoPlist.appGroupIdentifier) else {
|
guard let userDefaults = UserDefaults(suiteName: InfoPlistReader.target.appGroupIdentifier) else {
|
||||||
fatalError("Fail to load shared UserDefaults")
|
fatalError("Fail to load shared UserDefaults")
|
||||||
}
|
}
|
||||||
return userDefaults
|
return userDefaults
|
||||||
@ -63,9 +65,18 @@ final class ElementSettings: ObservableObject {
|
|||||||
|
|
||||||
@AppStorage(UserDefaultsKeys.timelineStyle.rawValue, store: store)
|
@AppStorage(UserDefaultsKeys.timelineStyle.rawValue, store: store)
|
||||||
var timelineStyle = BuildSettings.defaultRoomTimelineStyle
|
var timelineStyle = BuildSettings.defaultRoomTimelineStyle
|
||||||
|
|
||||||
// MARK: - Client
|
// MARK: - Client
|
||||||
|
|
||||||
@AppStorage(UserDefaultsKeys.slidingSyncProxyBaseURLString.rawValue, store: store)
|
@AppStorage(UserDefaultsKeys.slidingSyncProxyBaseURLString.rawValue, store: store)
|
||||||
var slidingSyncProxyBaseURLString = BuildSettings.defaultSlidingSyncProxyBaseURLString
|
var slidingSyncProxyBaseURLString = BuildSettings.defaultSlidingSyncProxyBaseURLString
|
||||||
|
|
||||||
|
// MARK: - Notifications
|
||||||
|
|
||||||
|
@AppStorage(UserDefaultsKeys.enableInAppNotifications.rawValue, store: store)
|
||||||
|
var enableInAppNotifications = true
|
||||||
|
|
||||||
|
@AppStorage(UserDefaultsKeys.pusherProfileTag.rawValue, store: store)
|
||||||
|
/// Tag describing which set of device specific rules a pusher executes.
|
||||||
|
var pusherProfileTag: String?
|
||||||
}
|
}
|
||||||
|
34
ElementX/Sources/Other/Extensions/FileManager.swift
Normal file
34
ElementX/Sources/Other/Extensions/FileManager.swift
Normal file
@ -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
|
||||||
|
|
||||||
|
extension FileManager {
|
||||||
|
func directoryExists(at url: URL) -> Bool {
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
guard fileExists(atPath: url.path(), isDirectory: &isDirectory) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isDirectory.boolValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDirectoryIfNeeded(at url: URL, withIntermediateDirectories: Bool = true) throws {
|
||||||
|
guard !directoryExists(at: url) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try createDirectory(at: url, withIntermediateDirectories: withIntermediateDirectories)
|
||||||
|
}
|
||||||
|
}
|
@ -23,4 +23,10 @@ extension ImageCache {
|
|||||||
result.diskStorage.config.sizeLimit = 1
|
result.diskStorage.config.sizeLimit = 1
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var onlyOnDisk: ImageCache {
|
||||||
|
let result = ImageCache.default
|
||||||
|
result.memoryStorage.config.totalCostLimit = 1
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,6 @@ extension UIDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var initialDisplayName: String {
|
var initialDisplayName: String {
|
||||||
ElementL10n.defaultSessionDisplayName(ElementInfoPlist.cfBundleDisplayName)
|
ElementL10n.defaultSessionDisplayName(InfoPlistReader.target.bundleDisplayName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,4 +24,33 @@ extension URL {
|
|||||||
|
|
||||||
self = url
|
self = url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The URL of the primary app group container.
|
||||||
|
static var appGroupContainerDirectory: URL {
|
||||||
|
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: InfoPlistReader.target.appGroupIdentifier) else {
|
||||||
|
fatalError("Should always be able to retrieve the container directory")
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The base directory where all session data is stored.
|
||||||
|
static var sessionsBaseDirectory: URL {
|
||||||
|
let url = cacheBaseDirectory
|
||||||
|
.appendingPathComponent("Sessions", isDirectory: true)
|
||||||
|
|
||||||
|
try? FileManager.default.createDirectoryIfNeeded(at: url)
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The base directory where all cache is stored.
|
||||||
|
static var cacheBaseDirectory: URL {
|
||||||
|
let url = appGroupContainerDirectory
|
||||||
|
.appendingPathComponent("Library", isDirectory: true)
|
||||||
|
.appendingPathComponent("Caches", isDirectory: true)
|
||||||
|
|
||||||
|
try? FileManager.default.createDirectoryIfNeeded(at: url)
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
79
ElementX/Sources/Other/InfoPlistReader.swift
Normal file
79
ElementX/Sources/Other/InfoPlistReader.swift
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
struct InfoPlistReader {
|
||||||
|
private enum Keys {
|
||||||
|
static let appGroupIdentifier = "appGroupIdentifier"
|
||||||
|
static let baseBundleIdentifier = "baseBundleIdentifier"
|
||||||
|
static let bundleShortVersion = "CFBundleShortVersionString"
|
||||||
|
static let bundleDisplayName = "CFBundleDisplayName"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Info.plist reader on the current target
|
||||||
|
static let target = InfoPlistReader(bundle: .main)
|
||||||
|
|
||||||
|
private let bundle: Bundle
|
||||||
|
|
||||||
|
/// Initializer
|
||||||
|
/// - Parameter bundle: bundle to read values from
|
||||||
|
init(bundle: Bundle) {
|
||||||
|
self.bundle = bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// App group identifier set in Info.plist of the target
|
||||||
|
var appGroupIdentifier: String {
|
||||||
|
infoPlistStringValue(forKey: Keys.appGroupIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base bundle identifier set in Info.plist of the target
|
||||||
|
var baseBundleIdentifier: String {
|
||||||
|
infoPlistStringValue(forKey: Keys.baseBundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bundle executable of the target
|
||||||
|
var bundleExecutable: String {
|
||||||
|
infoPlistStringValue(forKey: kCFBundleExecutableKey as String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bundle identifier of the target
|
||||||
|
var bundleIdentifier: String {
|
||||||
|
infoPlistStringValue(forKey: kCFBundleIdentifierKey as String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bundle short version string of the target
|
||||||
|
var bundleShortVersionString: String {
|
||||||
|
infoPlistStringValue(forKey: Keys.bundleShortVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bundle version of the target
|
||||||
|
var bundleVersion: String {
|
||||||
|
infoPlistStringValue(forKey: kCFBundleVersionKey as String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bundle display name of the target
|
||||||
|
var bundleDisplayName: String {
|
||||||
|
infoPlistStringValue(forKey: Keys.bundleDisplayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func infoPlistStringValue(forKey key: String) -> String {
|
||||||
|
guard let result = bundle.object(forInfoDictionaryKey: key) as? String else {
|
||||||
|
fatalError("Add \(key) into your target's Info.plst")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
@ -150,9 +150,9 @@ class MXLogger {
|
|||||||
MXLogger.logCrashes(false)
|
MXLogger.logCrashes(false)
|
||||||
|
|
||||||
// Extract running app information
|
// Extract running app information
|
||||||
let app = ElementInfoPlist.cfBundleExecutable
|
let app = InfoPlistReader.target.bundleExecutable
|
||||||
let appId = ElementInfoPlist.cfBundleIdentifier
|
let appId = InfoPlistReader.target.bundleIdentifier
|
||||||
let appVersion = "\(ElementInfoPlist.cfBundleShortVersionString) (r\(ElementInfoPlist.cfBundleVersion))"
|
let appVersion = "\(InfoPlistReader.target.bundleShortVersionString) (r\(InfoPlistReader.target.bundleVersion))"
|
||||||
|
|
||||||
// Build the crash log
|
// Build the crash log
|
||||||
let model = UIDevice.current.model
|
let model = UIDevice.current.model
|
||||||
@ -269,7 +269,7 @@ class MXLogger {
|
|||||||
|
|
||||||
/// The folder where logs are stored
|
/// The folder where logs are stored
|
||||||
private static var logsFolderURL: URL {
|
private static var logsFolderURL: URL {
|
||||||
FileManager.default.appGroupContainerURL ?? URL.documentsDirectory
|
.appGroupContainerDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If `self.redirectNSLog(toFiles:numberOfFiles:)` is called with a lower numberOfFiles we need to do some cleanup.
|
/// If `self.redirectNSLog(toFiles:numberOfFiles:)` is called with a lower numberOfFiles we need to do some cleanup.
|
||||||
|
@ -27,8 +27,8 @@ final class UserAgentBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class func makeUserAgent() -> String? {
|
private class func makeUserAgent() -> String? {
|
||||||
let clientName = ElementInfoPlist.cfBundleDisplayName
|
let clientName = InfoPlistReader.target.bundleDisplayName
|
||||||
let clientVersion = ElementInfoPlist.cfBundleShortVersionString
|
let clientVersion = InfoPlistReader.target.bundleShortVersionString
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
return String(
|
return String(
|
||||||
|
@ -44,10 +44,10 @@ struct AnalyticsPromptStrings {
|
|||||||
init() {
|
init() {
|
||||||
// Create the opt in content with a placeholder.
|
// Create the opt in content with a placeholder.
|
||||||
let linkPlaceholder = "{link}"
|
let linkPlaceholder = "{link}"
|
||||||
var optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleDisplayName, linkPlaceholder))
|
var optInContent = AttributedString(ElementL10n.analyticsOptInContent(InfoPlistReader.target.bundleDisplayName, linkPlaceholder))
|
||||||
|
|
||||||
guard let range = optInContent.range(of: linkPlaceholder) else {
|
guard let range = optInContent.range(of: linkPlaceholder) else {
|
||||||
self.optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleDisplayName,
|
self.optInContent = AttributedString(ElementL10n.analyticsOptInContent(InfoPlistReader.target.bundleDisplayName,
|
||||||
ElementL10n.analyticsOptInContentLink))
|
ElementL10n.analyticsOptInContentLink))
|
||||||
MXLog.failure("Failed to add a link attribute to the opt in content.")
|
MXLog.failure("Failed to add a link attribute to the opt in content.")
|
||||||
return
|
return
|
||||||
|
@ -67,7 +67,7 @@ struct AnalyticsPrompt: View {
|
|||||||
Image(uiImage: Asset.Images.analyticsLogo.image)
|
Image(uiImage: Asset.Images.analyticsLogo.image)
|
||||||
.padding(.bottom, 25)
|
.padding(.bottom, 25)
|
||||||
|
|
||||||
Text(ElementL10n.analyticsOptInTitle(ElementInfoPlist.cfBundleDisplayName))
|
Text(ElementL10n.analyticsOptInTitle(InfoPlistReader.target.bundleDisplayName))
|
||||||
.font(.element.title2Bold)
|
.font(.element.title2Bold)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.foregroundColor(.element.primaryContent)
|
.foregroundColor(.element.primaryContent)
|
||||||
|
@ -79,7 +79,7 @@ struct OnboardingViewState: BindableState {
|
|||||||
message: ElementL10n.ftueAuthCarouselEncryptedBody,
|
message: ElementL10n.ftueAuthCarouselEncryptedBody,
|
||||||
image: Asset.Images.onboardingScreenPage3),
|
image: Asset.Images.onboardingScreenPage3),
|
||||||
OnboardingPageContent(title: page4Title.tinting("."),
|
OnboardingPageContent(title: page4Title.tinting("."),
|
||||||
message: ElementL10n.ftueAuthCarouselWorkplaceBody(ElementInfoPlist.cfBundleDisplayName),
|
message: ElementL10n.ftueAuthCarouselWorkplaceBody(InfoPlistReader.target.bundleDisplayName),
|
||||||
image: Asset.Images.onboardingScreenPage4)
|
image: Asset.Images.onboardingScreenPage4)
|
||||||
]
|
]
|
||||||
bindings = OnboardingBindings()
|
bindings = OnboardingBindings()
|
||||||
|
@ -23,7 +23,7 @@ struct InviteFriendsCoordinator: CoordinatorProtocol {
|
|||||||
guard let permalink = try? PermalinkBuilder.permalinkTo(userIdentifier: userId).absoluteString else {
|
guard let permalink = try? PermalinkBuilder.permalinkTo(userIdentifier: userId).absoluteString else {
|
||||||
return AnyView(EmptyView())
|
return AnyView(EmptyView())
|
||||||
}
|
}
|
||||||
let shareText = ElementL10n.inviteFriendsText(ElementInfoPlist.cfBundleDisplayName, permalink)
|
let shareText = ElementL10n.inviteFriendsText(InfoPlistReader.target.bundleDisplayName, permalink)
|
||||||
|
|
||||||
return AnyView(UIActivityViewControllerWrapper(activityItems: [shareText])
|
return AnyView(UIActivityViewControllerWrapper(activityItems: [shareText])
|
||||||
.presentationDetents([.medium])
|
.presentationDetents([.medium])
|
||||||
|
@ -36,8 +36,8 @@ struct MessageComposer: View {
|
|||||||
focused: $focused,
|
focused: $focused,
|
||||||
maxHeight: 300,
|
maxHeight: 300,
|
||||||
onEnterKeyHandler: {
|
onEnterKeyHandler: {
|
||||||
sendAction()
|
sendAction()
|
||||||
})
|
})
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
sendAction()
|
sendAction()
|
||||||
|
@ -64,7 +64,7 @@ struct SettingsScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var versionText: some View {
|
private var versionText: some View {
|
||||||
Text(ElementL10n.settingsVersion + ": " + ElementInfoPlist.cfBundleShortVersionString + " (" + ElementInfoPlist.cfBundleVersion + ")")
|
Text(ElementL10n.settingsVersion + ": " + InfoPlistReader.target.bundleShortVersionString + " (" + InfoPlistReader.target.bundleVersion + ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var backgroundColor: Color {
|
private var backgroundColor: Color {
|
||||||
|
@ -92,7 +92,7 @@ class Analytics {
|
|||||||
|
|
||||||
// Catch and log crashes
|
// Catch and log crashes
|
||||||
// MXLogger.logCrashes(true)
|
// MXLogger.logCrashes(true)
|
||||||
// MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString)
|
// MXLogger.setBuildVersion(Bundle.bundleShortVersionString)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use the analytics settings from the supplied user session to configure analytics.
|
/// 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
|
// also enable logging crashes, to send them with bug reports
|
||||||
MXLogger.logCrashes(true)
|
MXLogger.logCrashes(true)
|
||||||
// set build version for logger
|
// set build version for logger
|
||||||
MXLogger.buildVersion = ElementInfoPlist.cfBundleShortVersionString
|
MXLogger.buildVersion = InfoPlistReader.target.bundleShortVersionString
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - BugReportServiceProtocol
|
// MARK: - BugReportServiceProtocol
|
||||||
@ -151,8 +151,8 @@ class BugReportService: BugReportServiceProtocol {
|
|||||||
return [
|
return [
|
||||||
MultipartFormData(key: "user_agent", type: .text(value: "iOS")),
|
MultipartFormData(key: "user_agent", type: .text(value: "iOS")),
|
||||||
MultipartFormData(key: "app", type: .text(value: applicationId)),
|
MultipartFormData(key: "app", type: .text(value: applicationId)),
|
||||||
MultipartFormData(key: "version", type: .text(value: ElementInfoPlist.cfBundleShortVersionString)),
|
MultipartFormData(key: "version", type: .text(value: InfoPlistReader.target.bundleShortVersionString)),
|
||||||
MultipartFormData(key: "build", type: .text(value: ElementInfoPlist.cfBundleVersion)),
|
MultipartFormData(key: "build", type: .text(value: InfoPlistReader.target.bundleVersion)),
|
||||||
MultipartFormData(key: "os", type: .text(value: os)),
|
MultipartFormData(key: "os", type: .text(value: os)),
|
||||||
MultipartFormData(key: "resolved_language", type: .text(value: Bundle.preferredLanguages[0])),
|
MultipartFormData(key: "resolved_language", type: .text(value: Bundle.preferredLanguages[0])),
|
||||||
MultipartFormData(key: "user_language", type: .text(value: Bundle.elementLanguage ?? "null")),
|
MultipartFormData(key: "user_language", type: .text(value: Bundle.elementLanguage ?? "null")),
|
||||||
|
@ -32,11 +32,11 @@ class FileCache {
|
|||||||
private let fileManager = FileManager.default
|
private let fileManager = FileManager.default
|
||||||
private let folder: URL
|
private let folder: URL
|
||||||
|
|
||||||
/// Default instance. Uses `FileCache` as the folder name.
|
/// Default instance. Uses `Files` as the folder name.
|
||||||
static let `default` = FileCache(folderName: "FileCache")
|
static let `default` = FileCache(folderName: "Files")
|
||||||
|
|
||||||
init(folderName: String) {
|
init(folderName: String) {
|
||||||
folder = fileManager.temporaryDirectory.appending(path: folderName, directoryHint: .isDirectory)
|
folder = URL.cacheBaseDirectory.appending(path: folderName, directoryHint: .isDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
@ -44,21 +44,6 @@ class FileCache {
|
|||||||
private func filePath(forKey key: String, fileExtension: String) -> URL {
|
private func filePath(forKey key: String, fileExtension: String) -> URL {
|
||||||
folder.appending(path: key, directoryHint: .notDirectory).appendingPathExtension(fileExtension)
|
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
|
// MARK: - FileCacheProtocol
|
||||||
@ -70,7 +55,7 @@ extension FileCache: FileCacheProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func store(_ data: Data, with fileExtension: String, forKey key: String) throws -> URL {
|
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)
|
let url = filePath(forKey: key, fileExtension: fileExtension)
|
||||||
try data.write(to: url)
|
try data.write(to: url)
|
||||||
return url
|
return url
|
||||||
@ -81,7 +66,7 @@ extension FileCache: FileCacheProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func removeAll() throws {
|
func removeAll() throws {
|
||||||
guard folderExists() else {
|
guard fileManager.directoryExists(at: folder) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try fileManager.removeItem(at: folder)
|
try fileManager.removeItem(at: folder)
|
||||||
|
@ -52,6 +52,7 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
private let client: ClientProtocol
|
private let client: ClientProtocol
|
||||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||||
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
|
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
|
||||||
|
private let mediaProxy: MediaProxyProtocol
|
||||||
private let clientQueue: DispatchQueue
|
private let clientQueue: DispatchQueue
|
||||||
|
|
||||||
private var slidingSyncObserverToken: StoppableSpawn?
|
private var slidingSyncObserverToken: StoppableSpawn?
|
||||||
@ -75,6 +76,8 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
self.backgroundTaskService = backgroundTaskService
|
self.backgroundTaskService = backgroundTaskService
|
||||||
clientQueue = .init(label: "ClientProxyQueue",
|
clientQueue = .init(label: "ClientProxyQueue",
|
||||||
attributes: .concurrent)
|
attributes: .concurrent)
|
||||||
|
mediaProxy = MediaProxy(client: client,
|
||||||
|
clientQueue: clientQueue)
|
||||||
|
|
||||||
await Task.dispatch(on: clientQueue) {
|
await Task.dispatch(on: clientQueue) {
|
||||||
do {
|
do {
|
||||||
@ -204,25 +207,7 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
.failure(.failedSettingAccountData)
|
.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> {
|
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
|
||||||
await Task.dispatch(on: clientQueue) {
|
await Task.dispatch(on: clientQueue) {
|
||||||
do {
|
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
|
// MARK: Private
|
||||||
|
|
||||||
@ -271,4 +282,29 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
|
|
||||||
callbacks.send(.receivedSyncUpdate)
|
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
|
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 callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
|
||||||
|
|
||||||
var userIdentifier: String { get }
|
var userIdentifier: String { get }
|
||||||
@ -62,13 +87,19 @@ protocol ClientProxyProtocol {
|
|||||||
|
|
||||||
func setAccountData<Content: Encodable>(content: Content, type: String) async -> Result<Void, ClientProxyError>
|
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 sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
|
||||||
|
|
||||||
func logout() async
|
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)
|
.failure(.failedSettingAccountData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
|
func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy {
|
||||||
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
|
.init(urlString: urlString)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
|
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
|
||||||
throw ClientProxyError.failedLoadingMedia
|
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
|
throw ClientProxyError.failedLoadingMedia
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,4 +72,18 @@ struct MockClientProxy: ClientProxyProtocol {
|
|||||||
func logout() async {
|
func logout() async {
|
||||||
// no-op
|
// 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 Foundation
|
||||||
import KeychainAccess
|
import KeychainAccess
|
||||||
|
|
||||||
|
enum KeychainControllerService: String {
|
||||||
|
case sessions
|
||||||
|
case tests
|
||||||
|
|
||||||
|
var identifier: String {
|
||||||
|
InfoPlistReader.target.baseBundleIdentifier + "." + rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class KeychainController: KeychainControllerProtocol {
|
class KeychainController: KeychainControllerProtocol {
|
||||||
private let keychain: Keychain
|
private let keychain: Keychain
|
||||||
|
|
||||||
init(identifier: String) {
|
init(service: KeychainControllerService,
|
||||||
keychain = Keychain(service: identifier)
|
accessGroup: String) {
|
||||||
|
keychain = Keychain(service: service.identifier,
|
||||||
|
accessGroup: accessGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) {
|
func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) {
|
||||||
do {
|
do {
|
||||||
let tokenData = try JSONEncoder().encode(restorationToken)
|
let tokenData = try JSONEncoder().encode(restorationToken)
|
||||||
@ -32,7 +43,7 @@ class KeychainController: KeychainControllerProtocol {
|
|||||||
MXLog.error("Failed storing user restore token with error: \(error)")
|
MXLog.error("Failed storing user restore token with error: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restorationTokenForUsername(_ username: String) -> RestorationToken? {
|
func restorationTokenForUsername(_ username: String) -> RestorationToken? {
|
||||||
do {
|
do {
|
||||||
guard let tokenData = try keychain.getData(username) else {
|
guard let tokenData = try keychain.getData(username) else {
|
||||||
@ -49,24 +60,24 @@ class KeychainController: KeychainControllerProtocol {
|
|||||||
homeserverUrl: legacyRestorationToken.homeURL,
|
homeserverUrl: legacyRestorationToken.homeURL,
|
||||||
isSoftLogout: legacyRestorationToken.isSoftLogout ?? false))
|
isSoftLogout: legacyRestorationToken.isSoftLogout ?? false))
|
||||||
}
|
}
|
||||||
|
|
||||||
return try JSONDecoder().decode(RestorationToken.self, from: tokenData)
|
return try JSONDecoder().decode(RestorationToken.self, from: tokenData)
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed retrieving user restore token")
|
MXLog.error("Failed retrieving user restore token")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restorationTokens() -> [KeychainCredentials] {
|
func restorationTokens() -> [KeychainCredentials] {
|
||||||
keychain.allKeys().compactMap { username in
|
keychain.allKeys().compactMap { username in
|
||||||
guard let restorationToken = restorationTokenForUsername(username) else {
|
guard let restorationToken = restorationTokenForUsername(username) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeychainCredentials(userID: username, restorationToken: restorationToken)
|
return KeychainCredentials(userID: username, restorationToken: restorationToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeRestorationTokenForUsername(_ username: String) {
|
func removeRestorationTokenForUsername(_ username: String) {
|
||||||
do {
|
do {
|
||||||
try keychain.remove(username)
|
try keychain.remove(username)
|
||||||
@ -74,7 +85,7 @@ class KeychainController: KeychainControllerProtocol {
|
|||||||
MXLog.error("Failed removing restore token with error: \(error)")
|
MXLog.error("Failed removing restore token with error: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeAllRestorationTokens() {
|
func removeAllRestorationTokens() {
|
||||||
do {
|
do {
|
||||||
try keychain.removeAll()
|
try keychain.removeAll()
|
@ -18,26 +18,26 @@ import Kingfisher
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct MediaProvider: MediaProviderProtocol {
|
struct MediaProvider: MediaProviderProtocol {
|
||||||
private let clientProxy: ClientProxyProtocol
|
private let mediaProxy: MediaProxyProtocol
|
||||||
private let imageCache: Kingfisher.ImageCache
|
private let imageCache: Kingfisher.ImageCache
|
||||||
private let fileCache: FileCache
|
private let fileCache: FileCache
|
||||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
private let backgroundTaskService: BackgroundTaskServiceProtocol?
|
||||||
|
|
||||||
init(clientProxy: ClientProxyProtocol,
|
init(mediaProxy: MediaProxyProtocol,
|
||||||
imageCache: Kingfisher.ImageCache,
|
imageCache: Kingfisher.ImageCache,
|
||||||
fileCache: FileCache,
|
fileCache: FileCache,
|
||||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
backgroundTaskService: BackgroundTaskServiceProtocol?) {
|
||||||
self.clientProxy = clientProxy
|
self.mediaProxy = mediaProxy
|
||||||
self.imageCache = imageCache
|
self.imageCache = imageCache
|
||||||
self.fileCache = fileCache
|
self.fileCache = fileCache
|
||||||
self.backgroundTaskService = backgroundTaskService
|
self.backgroundTaskService = backgroundTaskService
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? {
|
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? {
|
||||||
guard let source else {
|
guard let source else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), avatarSize: avatarSize)
|
let cacheKey = cacheKeyForURLString(source.url, avatarSize: avatarSize)
|
||||||
return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil)
|
return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,19 +46,19 @@ struct MediaProvider: MediaProviderProtocol {
|
|||||||
return nil
|
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> {
|
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) {
|
if let image = imageFromSource(source, avatarSize: avatarSize) {
|
||||||
return .success(image)
|
return .success(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadImageBgTask = await backgroundTaskService.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)")
|
let loadImageBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)")
|
||||||
defer {
|
defer {
|
||||||
loadImageBgTask?.stop()
|
loadImageBgTask?.stop()
|
||||||
}
|
}
|
||||||
@ -73,9 +73,9 @@ struct MediaProvider: MediaProviderProtocol {
|
|||||||
do {
|
do {
|
||||||
let imageData: Data
|
let imageData: Data
|
||||||
if let avatarSize {
|
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 {
|
} else {
|
||||||
imageData = try await clientProxy.loadMediaContentForSource(source.underlyingSource)
|
imageData = try await mediaProxy.loadMediaContentForSource(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let image = UIImage(data: imageData) else {
|
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 {
|
guard let source else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let cacheKey = fileCacheKeyForURLString(source.underlyingSource.url())
|
let cacheKey = fileCacheKeyForURLString(source.url)
|
||||||
return fileCache.file(forKey: cacheKey, fileExtension: fileExtension)
|
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) {
|
if let url = fileFromSource(source, fileExtension: fileExtension) {
|
||||||
return .success(url)
|
return .success(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadFileBgTask = await backgroundTaskService.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)")
|
let loadFileBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)")
|
||||||
defer {
|
defer {
|
||||||
loadFileBgTask?.stop()
|
loadFileBgTask?.stop()
|
||||||
}
|
}
|
||||||
@ -113,7 +113,7 @@ struct MediaProvider: MediaProviderProtocol {
|
|||||||
let cacheKey = fileCacheKeyForURLString(source.url)
|
let cacheKey = fileCacheKeyForURLString(source.url)
|
||||||
|
|
||||||
do {
|
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)
|
let url = try fileCache.store(data, with: fileExtension, forKey: cacheKey)
|
||||||
return .success(url)
|
return .success(url)
|
||||||
@ -128,12 +128,12 @@ struct MediaProvider: MediaProviderProtocol {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)),
|
return fileFromSource(MediaSourceProxy(urlString: urlString),
|
||||||
fileExtension: fileExtension)
|
fileExtension: fileExtension)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadFileFromURLString(_ urlString: String, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
func loadFileFromURLString(_ urlString: String, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||||
await loadFileFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)),
|
await loadFileFromSource(MediaSourceProxy(urlString: urlString),
|
||||||
fileExtension: fileExtension)
|
fileExtension: fileExtension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,17 +24,17 @@ enum MediaProviderError: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protocol MediaProviderProtocol {
|
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?
|
func imageFromURLString(_ urlString: String?, avatarSize: AvatarSize?) -> UIImage?
|
||||||
|
|
||||||
@discardableResult func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
|
@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?
|
func fileFromURLString(_ urlString: String?, fileExtension: String) -> URL?
|
||||||
|
|
||||||
@ -42,11 +42,11 @@ protocol MediaProviderProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension MediaProviderProtocol {
|
extension MediaProviderProtocol {
|
||||||
func imageFromSource(_ source: MediaSource?) -> UIImage? {
|
func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? {
|
||||||
imageFromSource(source, avatarSize: nil)
|
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)
|
await loadImageFromSource(source, avatarSize: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
49
ElementX/Sources/Services/Media/MediaProxy.swift
Normal file
49
ElementX/Sources/Services/Media/MediaProxy.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
ElementX/Sources/Services/Media/MediaProxyProtocol.swift
Normal file
25
ElementX/Sources/Services/Media/MediaProxyProtocol.swift
Normal file
@ -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 Foundation
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
|
||||||
struct MediaSource: Equatable {
|
struct MediaSourceProxy: Equatable {
|
||||||
let underlyingSource: MatrixRustSDK.MediaSource
|
let underlyingSource: MediaSource
|
||||||
|
|
||||||
|
init(source: MediaSource) {
|
||||||
|
underlyingSource = source
|
||||||
|
}
|
||||||
|
|
||||||
|
init(urlString: String) {
|
||||||
|
underlyingSource = mediaSourceFromUrl(url: urlString)
|
||||||
|
}
|
||||||
|
|
||||||
var url: String {
|
var url: String {
|
||||||
underlyingSource.url()
|
underlyingSource.url()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(source: MatrixRustSDK.MediaSource) {
|
|
||||||
underlyingSource = source
|
|
||||||
}
|
|
||||||
|
|
||||||
init(urlString: String) {
|
|
||||||
underlyingSource = MatrixRustSDK.mediaSourceFromUrl(url: urlString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Equatable
|
// MARK: - Equatable
|
||||||
|
|
||||||
static func == (lhs: MediaSource, rhs: MediaSource) -> Bool {
|
static func == (lhs: MediaSourceProxy, rhs: MediaSourceProxy) -> Bool {
|
||||||
lhs.underlyingSource.url() == rhs.underlyingSource.url()
|
lhs.url == rhs.url
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -18,11 +18,11 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct MockMediaProvider: MediaProviderProtocol {
|
struct MockMediaProvider: MediaProviderProtocol {
|
||||||
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? {
|
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
|
func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
|
||||||
.failure(.failedRetrievingImage)
|
.failure(.failedRetrievingImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,11 +50,11 @@ struct MockMediaProvider: MediaProviderProtocol {
|
|||||||
return .success(image)
|
return .success(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL? {
|
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL? {
|
||||||
nil
|
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)
|
.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
|
import Foundation
|
||||||
|
|
||||||
extension FileManager {
|
class MockNotificationServiceProxy: NotificationServiceProxyProtocol {
|
||||||
/// The URL of the primary app group container.
|
func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy? {
|
||||||
@objc var appGroupContainerURL: URL? {
|
nil
|
||||||
containerURL(forSecurityApplicationGroupIdentifier: ElementInfoPlist.appGroupIdentifier)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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`.
|
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.image`.
|
||||||
extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent {
|
extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent {
|
||||||
var source: MediaSource {
|
var source: MediaSourceProxy {
|
||||||
MediaSource(source: content.source)
|
.init(source: content.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
var width: CGFloat? {
|
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`.
|
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.video`.
|
||||||
extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent {
|
extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent {
|
||||||
var source: MediaSource {
|
var source: MediaSourceProxy {
|
||||||
MediaSource(source: content.source)
|
.init(source: content.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
var thumbnailSource: MediaSource? {
|
var thumbnailSource: MediaSourceProxy? {
|
||||||
guard let src = content.info?.thumbnailSource else {
|
guard let src = content.info?.thumbnailSource else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return MediaSource(source: src)
|
return .init(source: src)
|
||||||
}
|
}
|
||||||
|
|
||||||
var duration: UInt64 {
|
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`.
|
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.file`.
|
||||||
extension MessageTimelineItem where Content == MatrixRustSDK.FileMessageContent {
|
extension MessageTimelineItem where Content == MatrixRustSDK.FileMessageContent {
|
||||||
var source: MediaSource {
|
var source: MediaSourceProxy {
|
||||||
MediaSource(source: content.source)
|
.init(source: content.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
var thumbnailSource: MediaSource? {
|
var thumbnailSource: MediaSourceProxy? {
|
||||||
guard let src = content.info?.thumbnailSource else {
|
guard let src = content.info?.thumbnailSource else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return MediaSource(source: src)
|
return .init(source: src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,8 @@ struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat
|
|||||||
var senderDisplayName: String?
|
var senderDisplayName: String?
|
||||||
var senderAvatar: UIImage?
|
var senderAvatar: UIImage?
|
||||||
|
|
||||||
let source: MediaSource?
|
let source: MediaSourceProxy?
|
||||||
let thumbnailSource: MediaSource?
|
let thumbnailSource: MediaSourceProxy?
|
||||||
var cachedFileURL: URL?
|
var cachedFileURL: URL?
|
||||||
|
|
||||||
var properties = RoomTimelineItemProperties()
|
var properties = RoomTimelineItemProperties()
|
||||||
|
@ -29,7 +29,7 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
|
|||||||
var senderDisplayName: String?
|
var senderDisplayName: String?
|
||||||
var senderAvatar: UIImage?
|
var senderAvatar: UIImage?
|
||||||
|
|
||||||
let source: MediaSource?
|
let source: MediaSourceProxy?
|
||||||
var image: UIImage?
|
var image: UIImage?
|
||||||
|
|
||||||
var width: CGFloat?
|
var width: CGFloat?
|
||||||
|
@ -30,8 +30,8 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
|
|||||||
var senderAvatar: UIImage?
|
var senderAvatar: UIImage?
|
||||||
|
|
||||||
let duration: UInt64
|
let duration: UInt64
|
||||||
let source: MediaSource?
|
let source: MediaSourceProxy?
|
||||||
let thumbnailSource: MediaSource?
|
let thumbnailSource: MediaSourceProxy?
|
||||||
var image: UIImage?
|
var image: UIImage?
|
||||||
var cachedVideoURL: URL?
|
var cachedVideoURL: URL?
|
||||||
|
|
||||||
|
@ -42,7 +42,17 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
func start() {
|
func start() {
|
||||||
stateMachine.processEvent(.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
|
// MARK: - Private
|
||||||
|
|
||||||
// swiftlint:disable:next cyclomatic_complexity
|
// swiftlint:disable:next cyclomatic_complexity
|
||||||
|
@ -150,4 +150,14 @@ class UserSessionFlowCoordinatorStateMachine {
|
|||||||
func addErrorHandler(_ handler: @escaping StateMachine<State, Event>.Handler) {
|
func addErrorHandler(_ handler: @escaping StateMachine<State, Event>.Handler) {
|
||||||
stateMachine.addErrorHandler(handler: 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 }
|
var hasSessions: Bool { !keychainController.restorationTokens().isEmpty }
|
||||||
|
|
||||||
/// The base directory where all session data is stored.
|
/// The base directory where all session data is stored.
|
||||||
private(set) lazy var baseDirectory: URL = {
|
let 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
|
|
||||||
}()
|
|
||||||
|
|
||||||
init(bundleIdentifier: String, backgroundTaskService: BackgroundTaskServiceProtocol) {
|
init(backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||||
keychainController = KeychainController(identifier: bundleIdentifier)
|
keychainController = KeychainController(service: .sessions,
|
||||||
|
accessGroup: InfoPlistReader.target.appGroupIdentifier)
|
||||||
self.backgroundTaskService = backgroundTaskService
|
self.backgroundTaskService = backgroundTaskService
|
||||||
|
baseDirectory = .sessionsBaseDirectory
|
||||||
|
MXLog.debug("Setup base directory at: \(baseDirectory)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError> {
|
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError> {
|
||||||
@ -58,7 +46,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
|||||||
switch await restorePreviousLogin(credentials) {
|
switch await restorePreviousLogin(credentials) {
|
||||||
case .success(let clientProxy):
|
case .success(let clientProxy):
|
||||||
return .success(UserSession(clientProxy: clientProxy,
|
return .success(UserSession(clientProxy: clientProxy,
|
||||||
mediaProvider: MediaProvider(clientProxy: clientProxy,
|
mediaProvider: MediaProvider(mediaProxy: clientProxy,
|
||||||
imageCache: .onlyInMemory,
|
imageCache: .onlyInMemory,
|
||||||
fileCache: .default,
|
fileCache: .default,
|
||||||
backgroundTaskService: backgroundTaskService)))
|
backgroundTaskService: backgroundTaskService)))
|
||||||
@ -77,7 +65,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
|||||||
switch await setupProxyForClient(client) {
|
switch await setupProxyForClient(client) {
|
||||||
case .success(let clientProxy):
|
case .success(let clientProxy):
|
||||||
return .success(UserSession(clientProxy: clientProxy,
|
return .success(UserSession(clientProxy: clientProxy,
|
||||||
mediaProvider: MediaProvider(clientProxy: clientProxy,
|
mediaProvider: MediaProvider(mediaProxy: clientProxy,
|
||||||
imageCache: .onlyInMemory,
|
imageCache: .onlyInMemory,
|
||||||
fileCache: .default,
|
fileCache: .default,
|
||||||
backgroundTaskService: backgroundTaskService)))
|
backgroundTaskService: backgroundTaskService)))
|
||||||
|
@ -17,8 +17,9 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class UITestsAppCoordinator: CoordinatorProtocol {
|
class UITestsAppCoordinator: AppCoordinatorProtocol {
|
||||||
private let navigationController: NavigationController
|
private let navigationController: NavigationController
|
||||||
|
let notificationManager: NotificationManagerProtocol? = nil
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
navigationController = NavigationController()
|
navigationController = NavigationController()
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.usernotifications.communication</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
|
@ -22,6 +22,10 @@
|
|||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>NSUserActivityTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>INSendMessageIntent</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
@ -33,5 +37,7 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>appGroupIdentifier</key>
|
<key>appGroupIdentifier</key>
|
||||||
<string>$(APP_GROUP_IDENTIFIER)</string>
|
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||||
|
<key>baseBundleIdentifier</key>
|
||||||
|
<string>$(BASE_BUNDLE_IDENTIFIER)</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -46,15 +46,19 @@ targets:
|
|||||||
UIInterfaceOrientationLandscapeRight
|
UIInterfaceOrientationLandscapeRight
|
||||||
]
|
]
|
||||||
appGroupIdentifier: $(APP_GROUP_IDENTIFIER)
|
appGroupIdentifier: $(APP_GROUP_IDENTIFIER)
|
||||||
|
baseBundleIdentifier: $(BASE_BUNDLE_IDENTIFIER)
|
||||||
ITSAppUsesNonExemptEncryption: false
|
ITSAppUsesNonExemptEncryption: false
|
||||||
|
NSUserActivityTypes: [
|
||||||
|
INSendMessageIntent
|
||||||
|
]
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
PRODUCT_NAME: ElementX
|
PRODUCT_NAME: ElementX
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER)
|
PRODUCT_BUNDLE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER)
|
||||||
MARKETING_VERSION: 1.0.9
|
MARKETING_VERSION: $(MARKETING_VERSION)
|
||||||
CURRENT_PROJECT_VERSION: 1
|
CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION)
|
||||||
DEVELOPMENT_TEAM: 7J4U792NQT
|
DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM)
|
||||||
CODE_SIGN_ENTITLEMENTS: ElementX/SupportingFiles/ElementX.entitlements
|
CODE_SIGN_ENTITLEMENTS: ElementX/SupportingFiles/ElementX.entitlements
|
||||||
SWIFT_OBJC_BRIDGING_HEADER: ElementX/SupportingFiles/ElementX-Bridging-Header.h
|
SWIFT_OBJC_BRIDGING_HEADER: ElementX/SupportingFiles/ElementX-Bridging-Header.h
|
||||||
SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h
|
SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h
|
||||||
@ -97,6 +101,7 @@ targets:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
- target: NSE
|
||||||
- package: MatrixRustSDK
|
- package: MatrixRustSDK
|
||||||
- package: DesignKit
|
- package: DesignKit
|
||||||
- package: AnalyticsEvents
|
- package: AnalyticsEvents
|
||||||
|
165
NSE/Sources/NotificationServiceExtension.swift
Normal file
165
NSE/Sources/NotificationServiceExtension.swift
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
45
NSE/Sources/Other/DataProtectionManager.swift
Normal file
45
NSE/Sources/Other/DataProtectionManager.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
110
NSE/Sources/Other/NSELogger.swift
Normal file
110
NSE/Sources/Other/NSELogger.swift
Normal file
@ -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)")
|
||||||
|
}
|
||||||
|
}
|
222
NSE/Sources/Other/NotificationItemProxy+NSE.swift
Normal file
222
NSE/Sources/Other/NotificationItemProxy+NSE.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
91
NSE/Sources/Other/UNMutableNotificationContent.swift
Normal file
91
NSE/Sources/Other/UNMutableNotificationContent.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
NSE/Sources/Other/UNNotificationRequest.swift
Normal file
32
NSE/Sources/Other/UNNotificationRequest.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
33
NSE/SupportingFiles/Info.plist
Normal file
33
NSE/SupportingFiles/Info.plist
Normal file
@ -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>
|
12
NSE/SupportingFiles/NSE.entitlements
Normal file
12
NSE/SupportingFiles/NSE.entitlements
Normal file
@ -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>
|
89
NSE/SupportingFiles/target.yml
Normal file
89
NSE/SupportingFiles/target.yml
Normal file
@ -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
|
@ -24,10 +24,3 @@ strings:
|
|||||||
params:
|
params:
|
||||||
enumName: ElementL10n
|
enumName: ElementL10n
|
||||||
publicAccess: true
|
publicAccess: true
|
||||||
plist:
|
|
||||||
inputs: SupportingFiles/Info.plist
|
|
||||||
outputs:
|
|
||||||
templatePath: Templates/Plists/runtime-swift5-element-info.stencil
|
|
||||||
output: InfoPlist.swift
|
|
||||||
params:
|
|
||||||
enumName: ElementInfoPlist
|
|
||||||
|
@ -70,8 +70,8 @@ targets:
|
|||||||
basedOnDependencyAnalysis: false
|
basedOnDependencyAnalysis: false
|
||||||
shell: /bin/sh
|
shell: /bin/sh
|
||||||
script: |
|
script: |
|
||||||
python3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPhone 13 Pro Max' --version 'iOS.16.0'
|
python3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPhone 13 Pro Max' --version 'iOS.16.1'
|
||||||
python3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPad (9th generation)' --version 'iOS.16.0'
|
python3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPad (9th generation)' --version 'iOS.16.1'
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
- path: ../Sources
|
- path: ../Sources
|
||||||
@ -85,7 +85,8 @@ targets:
|
|||||||
- path: ../../ElementX/Sources/UITests/UITestScreenIdentifier.swift
|
- path: ../../ElementX/Sources/UITests/UITestScreenIdentifier.swift
|
||||||
- path: ../../ElementX/Sources/Generated/Strings.swift
|
- path: ../../ElementX/Sources/Generated/Strings.swift
|
||||||
- path: ../../ElementX/Sources/Generated/Strings+Untranslated.swift
|
- path: ../../ElementX/Sources/Generated/Strings+Untranslated.swift
|
||||||
- path: ../../ElementX/Sources/Generated/InfoPlist.swift
|
|
||||||
- path: ../../ElementX/Resources
|
- path: ../../ElementX/Resources
|
||||||
- path: ../../ElementX/Sources/Other/Extensions/Bundle.swift
|
- path: ../../ElementX/Sources/Other/Extensions/Bundle.swift
|
||||||
|
- path: ../../ElementX/Sources/Other/Extensions/FileManager.swift
|
||||||
|
- path: ../../ElementX/Sources/Other/InfoPlistReader.swift
|
||||||
- path: ../../ElementX/Sources/Other/Extensions/URL.swift
|
- path: ../../ElementX/Sources/Other/Extensions/URL.swift
|
||||||
|
@ -21,7 +21,8 @@ class KeychainControllerTests: XCTestCase {
|
|||||||
var keychain: KeychainController!
|
var keychain: KeychainController!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
keychain = KeychainController(identifier: "\(ElementInfoPlist.cfBundleIdentifier).tests")
|
keychain = KeychainController(service: .tests,
|
||||||
|
accessGroup: InfoPlistReader.target.appGroupIdentifier)
|
||||||
keychain.removeAllRestorationTokens()
|
keychain.removeAllRestorationTokens()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,11 +25,11 @@ class UserAgentBuilderTests: XCTestCase {
|
|||||||
|
|
||||||
func testContainsClientName() {
|
func testContainsClientName() {
|
||||||
let userAgent = UserAgentBuilder.makeASCIIUserAgent()
|
let userAgent = UserAgentBuilder.makeASCIIUserAgent()
|
||||||
XCTAssert(userAgent?.contains(ElementInfoPlist.cfBundleDisplayName) == true, "\(userAgent ?? "nil") does not contain client name")
|
XCTAssert(userAgent?.contains(InfoPlistReader.target.bundleDisplayName) == true, "\(userAgent ?? "nil") does not contain client name")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testContainsClientVersion() {
|
func testContainsClientVersion() {
|
||||||
let userAgent = UserAgentBuilder.makeASCIIUserAgent()
|
let userAgent = UserAgentBuilder.makeASCIIUserAgent()
|
||||||
XCTAssert(userAgent?.contains(ElementInfoPlist.cfBundleShortVersionString) == true, "\(userAgent ?? "nil") does not contain client version")
|
XCTAssert(userAgent?.contains(InfoPlistReader.target.bundleShortVersionString) == true, "\(userAgent ?? "nil") does not contain client version")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,4 +49,4 @@ targets:
|
|||||||
- path: ../SupportingFiles
|
- path: ../SupportingFiles
|
||||||
- path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit
|
- path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit
|
||||||
- path: ../Resources
|
- path: ../Resources
|
||||||
|
- path: ../../ElementX/Sources/Other/InfoPlistReader.swift
|
||||||
|
1
changelog.d/243.feature
Normal file
1
changelog.d/243.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
NSE: Configure target with commented code blocks.
|
@ -25,12 +25,16 @@ settings:
|
|||||||
BASE_APP_GROUP_IDENTIFIER: io.element
|
BASE_APP_GROUP_IDENTIFIER: io.element
|
||||||
APP_GROUP_IDENTIFIER: group.$(BASE_APP_GROUP_IDENTIFIER)
|
APP_GROUP_IDENTIFIER: group.$(BASE_APP_GROUP_IDENTIFIER)
|
||||||
BASE_BUNDLE_IDENTIFIER: io.element.elementx
|
BASE_BUNDLE_IDENTIFIER: io.element.elementx
|
||||||
|
MARKETING_VERSION: 1.0.9
|
||||||
|
CURRENT_PROJECT_VERSION: 1
|
||||||
|
DEVELOPMENT_TEAM: 7J4U792NQT
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- path: ElementX/SupportingFiles/target.yml
|
- path: ElementX/SupportingFiles/target.yml
|
||||||
- path: UnitTests/SupportingFiles/target.yml
|
- path: UnitTests/SupportingFiles/target.yml
|
||||||
- path: UITests/SupportingFiles/target.yml
|
- path: UITests/SupportingFiles/target.yml
|
||||||
- path: IntegrationTests/SupportingFiles/target.yml
|
- path: IntegrationTests/SupportingFiles/target.yml
|
||||||
|
- path: NSE/SupportingFiles/target.yml
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
MatrixRustSDK:
|
MatrixRustSDK:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user