Notifications (#275)

This commit is contained in:
ismailgulek 2022-11-21 19:37:13 +03:00 committed by GitHub
parent 9f3ed6ca7b
commit d389ce7ad7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2459 additions and 324 deletions

File diff suppressed because it is too large Load Diff

View 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>

View File

@ -31,3 +31,5 @@
// Parameter is the application display name (e.g. "ElementX")
"default_session_display_name" = "%@ iOS";
"Notification" = "Notification";

Binary file not shown.

View File

@ -31,7 +31,7 @@ struct ServiceLocator {
let userNotificationController: UserNotificationControllerProtocol
}
class AppCoordinator: CoordinatorProtocol {
class AppCoordinator: AppCoordinatorProtocol {
private let stateMachine: AppCoordinatorStateMachine
private let navigationController: NavigationController
private let userSessionStore: UserSessionStoreProtocol
@ -40,6 +40,7 @@ class AppCoordinator: CoordinatorProtocol {
didSet {
deobserveUserSessionChanges()
if let userSession, !userSession.isSoftLogout {
configureNotificationManager()
observeUserSessionChanges()
}
}
@ -52,6 +53,7 @@ class AppCoordinator: CoordinatorProtocol {
private let backgroundTaskService: BackgroundTaskServiceProtocol
private var cancellables = Set<AnyCancellable>()
private(set) var notificationManager: NotificationManagerProtocol?
init() {
navigationController = NavigationController()
@ -63,14 +65,9 @@ class AppCoordinator: CoordinatorProtocol {
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)
userSessionStore = UserSessionStore(bundleIdentifier: bundleIdentifier,
backgroundTaskService: backgroundTaskService)
userSessionStore = UserSessionStore(backgroundTaskService: backgroundTaskService)
setupStateMachine()
@ -97,6 +94,7 @@ class AppCoordinator: CoordinatorProtocol {
private func setupLogging() {
let loggerConfiguration = MXLogConfiguration()
loggerConfiguration.maxLogFilesCount = 10
#if DEBUG
// 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
userSessionStore.logout(userSession: userSession)
userSession = nil
notificationManager?.delegate = nil
notificationManager = nil
}
}
@ -269,6 +269,38 @@ class AppCoordinator: CoordinatorProtocol {
}
}
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() {
userSession.callbacks
.receive(on: DispatchQueue.main)
@ -321,3 +353,51 @@ extension AppCoordinator: AuthenticationCoordinatorDelegate {
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)
}
}
}

View 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 }
}

View File

@ -14,38 +14,30 @@
// limitations under the License.
//
import SwiftUI
import Combine
import Foundation
import UIKit
@main
struct Application: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var applicationDelegate
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()
}
}
}
}
enum AppDelegateCallback {
case registeredNotifications(deviceToken: Data)
case failedToRegisteredNotifications(error: Error)
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
true
private(set) static var shared: AppDelegate!
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))
}
}

View 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()
}
}
}
}
}

View File

@ -17,11 +17,21 @@
import Foundation
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
static let defaultHomeserverAddress = "matrix.org"
static let defaultSlidingSyncProxyBaseURLString = "https://slidingsync.lab.element.dev"
static let pushGatewayBaseURL = URL(staticString: "https://matrix.org/_matrix/push/v1/notify")
// MARK: - Bug report
@ -38,14 +48,14 @@ final class BuildSettings {
#if DEBUG
/// 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.
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",
apiKey: "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN",
termsURL: URL(staticString: "https://element.io/cookie-policy"))
#else
/// 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.
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",
apiKey: "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
termsURL: URL(staticString: "https://element.io/cookie-policy"))
@ -63,4 +73,8 @@ final class BuildSettings {
// MARK: - Other
static var permalinkBaseURL = URL(staticString: "https://matrix.to")
// MARK: - Notifications
static let enableNotifications = false
}

View File

@ -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

View File

@ -24,6 +24,8 @@ extension ElementL10n {
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
/// Tablet
public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device")
/// Notification
public static let notification = ElementL10n.tr("Untranslated", "Notification")
/// Editing
public static let roomTimelineEditing = ElementL10n.tr("Untranslated", "room_timeline_editing")
/// Failed creating the permalink

View File

@ -26,13 +26,15 @@ final class ElementSettings: ObservableObject {
case enableAnalytics
case isIdentifiedForAnalytics
case slidingSyncProxyBaseURLString
case enableInAppNotifications
case pusherProfileTag
}
static let shared = ElementSettings()
/// UserDefaults to be used on reads and writes.
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")
}
return userDefaults
@ -68,4 +70,13 @@ final class ElementSettings: ObservableObject {
@AppStorage(UserDefaultsKeys.slidingSyncProxyBaseURLString.rawValue, store: store)
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?
}

View 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)
}
}

View File

@ -23,4 +23,10 @@ extension ImageCache {
result.diskStorage.config.sizeLimit = 1
return result
}
static var onlyOnDisk: ImageCache {
let result = ImageCache.default
result.memoryStorage.config.totalCostLimit = 1
return result
}
}

View File

@ -23,6 +23,6 @@ extension UIDevice {
}
var initialDisplayName: String {
ElementL10n.defaultSessionDisplayName(ElementInfoPlist.cfBundleDisplayName)
ElementL10n.defaultSessionDisplayName(InfoPlistReader.target.bundleDisplayName)
}
}

View File

@ -24,4 +24,33 @@ extension 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
}
}

View 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
}
}

View File

@ -150,9 +150,9 @@ class MXLogger {
MXLogger.logCrashes(false)
// Extract running app information
let app = ElementInfoPlist.cfBundleExecutable
let appId = ElementInfoPlist.cfBundleIdentifier
let appVersion = "\(ElementInfoPlist.cfBundleShortVersionString) (r\(ElementInfoPlist.cfBundleVersion))"
let app = InfoPlistReader.target.bundleExecutable
let appId = InfoPlistReader.target.bundleIdentifier
let appVersion = "\(InfoPlistReader.target.bundleShortVersionString) (r\(InfoPlistReader.target.bundleVersion))"
// Build the crash log
let model = UIDevice.current.model
@ -269,7 +269,7 @@ class MXLogger {
/// The folder where logs are stored
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.

View File

@ -27,8 +27,8 @@ final class UserAgentBuilder {
}
private class func makeUserAgent() -> String? {
let clientName = ElementInfoPlist.cfBundleDisplayName
let clientVersion = ElementInfoPlist.cfBundleShortVersionString
let clientName = InfoPlistReader.target.bundleDisplayName
let clientVersion = InfoPlistReader.target.bundleShortVersionString
#if os(iOS)
return String(

View File

@ -44,10 +44,10 @@ struct AnalyticsPromptStrings {
init() {
// Create the opt in content with a placeholder.
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 {
self.optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleDisplayName,
self.optInContent = AttributedString(ElementL10n.analyticsOptInContent(InfoPlistReader.target.bundleDisplayName,
ElementL10n.analyticsOptInContentLink))
MXLog.failure("Failed to add a link attribute to the opt in content.")
return

View File

@ -67,7 +67,7 @@ struct AnalyticsPrompt: View {
Image(uiImage: Asset.Images.analyticsLogo.image)
.padding(.bottom, 25)
Text(ElementL10n.analyticsOptInTitle(ElementInfoPlist.cfBundleDisplayName))
Text(ElementL10n.analyticsOptInTitle(InfoPlistReader.target.bundleDisplayName))
.font(.element.title2Bold)
.multilineTextAlignment(.center)
.foregroundColor(.element.primaryContent)

View File

@ -79,7 +79,7 @@ struct OnboardingViewState: BindableState {
message: ElementL10n.ftueAuthCarouselEncryptedBody,
image: Asset.Images.onboardingScreenPage3),
OnboardingPageContent(title: page4Title.tinting("."),
message: ElementL10n.ftueAuthCarouselWorkplaceBody(ElementInfoPlist.cfBundleDisplayName),
message: ElementL10n.ftueAuthCarouselWorkplaceBody(InfoPlistReader.target.bundleDisplayName),
image: Asset.Images.onboardingScreenPage4)
]
bindings = OnboardingBindings()

View File

@ -23,7 +23,7 @@ struct InviteFriendsCoordinator: CoordinatorProtocol {
guard let permalink = try? PermalinkBuilder.permalinkTo(userIdentifier: userId).absoluteString else {
return AnyView(EmptyView())
}
let shareText = ElementL10n.inviteFriendsText(ElementInfoPlist.cfBundleDisplayName, permalink)
let shareText = ElementL10n.inviteFriendsText(InfoPlistReader.target.bundleDisplayName, permalink)
return AnyView(UIActivityViewControllerWrapper(activityItems: [shareText])
.presentationDetents([.medium])

View File

@ -36,8 +36,8 @@ struct MessageComposer: View {
focused: $focused,
maxHeight: 300,
onEnterKeyHandler: {
sendAction()
})
sendAction()
})
Button {
sendAction()

View File

@ -64,7 +64,7 @@ struct SettingsScreen: 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 {

View File

@ -92,7 +92,7 @@ class Analytics {
// Catch and log crashes
// MXLogger.logCrashes(true)
// MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString)
// MXLogger.setBuildVersion(Bundle.bundleShortVersionString)
}
/// Use the analytics settings from the supplied user session to configure analytics.

View File

@ -61,7 +61,7 @@ class BugReportService: BugReportServiceProtocol {
// also enable logging crashes, to send them with bug reports
MXLogger.logCrashes(true)
// set build version for logger
MXLogger.buildVersion = ElementInfoPlist.cfBundleShortVersionString
MXLogger.buildVersion = InfoPlistReader.target.bundleShortVersionString
}
// MARK: - BugReportServiceProtocol
@ -151,8 +151,8 @@ class BugReportService: BugReportServiceProtocol {
return [
MultipartFormData(key: "user_agent", type: .text(value: "iOS")),
MultipartFormData(key: "app", type: .text(value: applicationId)),
MultipartFormData(key: "version", type: .text(value: ElementInfoPlist.cfBundleShortVersionString)),
MultipartFormData(key: "build", type: .text(value: ElementInfoPlist.cfBundleVersion)),
MultipartFormData(key: "version", type: .text(value: InfoPlistReader.target.bundleShortVersionString)),
MultipartFormData(key: "build", type: .text(value: InfoPlistReader.target.bundleVersion)),
MultipartFormData(key: "os", type: .text(value: os)),
MultipartFormData(key: "resolved_language", type: .text(value: Bundle.preferredLanguages[0])),
MultipartFormData(key: "user_language", type: .text(value: Bundle.elementLanguage ?? "null")),

View File

@ -32,11 +32,11 @@ class FileCache {
private let fileManager = FileManager.default
private let folder: URL
/// Default instance. Uses `FileCache` as the folder name.
static let `default` = FileCache(folderName: "FileCache")
/// Default instance. Uses `Files` as the folder name.
static let `default` = FileCache(folderName: "Files")
init(folderName: String) {
folder = fileManager.temporaryDirectory.appending(path: folderName, directoryHint: .isDirectory)
folder = URL.cacheBaseDirectory.appending(path: folderName, directoryHint: .isDirectory)
}
// MARK: Private
@ -44,21 +44,6 @@ class FileCache {
private func filePath(forKey key: String, fileExtension: String) -> URL {
folder.appending(path: key, directoryHint: .notDirectory).appendingPathExtension(fileExtension)
}
private func folderExists() -> Bool {
var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: folder.path(), isDirectory: &isDirectory) else {
return false
}
return isDirectory.boolValue
}
private func createFolderIfNeeded() throws {
guard !folderExists() else {
return
}
try fileManager.createDirectory(at: folder, withIntermediateDirectories: true)
}
}
// MARK: - FileCacheProtocol
@ -70,7 +55,7 @@ extension FileCache: FileCacheProtocol {
}
func store(_ data: Data, with fileExtension: String, forKey key: String) throws -> URL {
try createFolderIfNeeded()
try fileManager.createDirectoryIfNeeded(at: folder)
let url = filePath(forKey: key, fileExtension: fileExtension)
try data.write(to: url)
return url
@ -81,7 +66,7 @@ extension FileCache: FileCacheProtocol {
}
func removeAll() throws {
guard folderExists() else {
guard fileManager.directoryExists(at: folder) else {
return
}
try fileManager.removeItem(at: folder)

View File

@ -52,6 +52,7 @@ class ClientProxy: ClientProxyProtocol {
private let client: ClientProtocol
private let backgroundTaskService: BackgroundTaskServiceProtocol
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
private let mediaProxy: MediaProxyProtocol
private let clientQueue: DispatchQueue
private var slidingSyncObserverToken: StoppableSpawn?
@ -75,6 +76,8 @@ class ClientProxy: ClientProxyProtocol {
self.backgroundTaskService = backgroundTaskService
clientQueue = .init(label: "ClientProxyQueue",
attributes: .concurrent)
mediaProxy = MediaProxy(client: client,
clientQueue: clientQueue)
await Task.dispatch(on: clientQueue) {
do {
@ -205,24 +208,6 @@ class ClientProxy: ClientProxyProtocol {
}
}
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
}
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
try await Task.dispatch(on: clientQueue) {
let bytes = try self.client.getMediaContent(source: source)
return Data(bytes: bytes, count: bytes.count)
}
}
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data {
try await Task.dispatch(on: clientQueue) {
let bytes = try self.client.getMediaThumbnail(source: source, width: UInt64(width), height: UInt64(height))
return Data(bytes: bytes, count: bytes.count)
}
}
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
await Task.dispatch(on: clientQueue) {
do {
@ -244,6 +229,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
private func roomTupleForIdentifier(_ identifier: String) -> (SlidingSyncRoom?, Room?) {
@ -271,4 +282,29 @@ class ClientProxy: ClientProxyProtocol {
callbacks.send(.receivedSyncUpdate)
}
/// Convenience method to get the json string of an Encodable
private func jsonString(from dictionary: [AnyHashable: Any]?) -> String? {
guard let dictionary,
let data = try? JSONSerialization.data(withJSONObject: dictionary,
options: [.fragmentsAllowed]) else {
return nil
}
return String(data: data, encoding: .utf8)
}
}
extension ClientProxy: MediaProxyProtocol {
func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy {
mediaProxy.mediaSourceForURLString(urlString)
}
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
try await mediaProxy.loadMediaContentForSource(source)
}
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
try await mediaProxy.loadMediaThumbnailForSource(source, width: width, height: height)
}
}

View File

@ -33,7 +33,32 @@ enum ClientProxyError: Error {
case failedLoadingMedia
}
protocol ClientProxyProtocol {
enum PusherKind {
case http
case email
// var rustValue: MatrixRustSDK.PusherKind {
// switch self {
// case .http:
// return .http
// case .email:
// return .email
// }
// }
}
enum PushFormat {
case eventIdOnly
// var rustValue: MatrixRustSDK.PushFormat {
// switch self {
// case .eventIdOnly:
// return .eventIdOnly
// }
// }
}
protocol ClientProxyProtocol: MediaProxyProtocol {
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
var userIdentifier: String { get }
@ -62,13 +87,19 @@ protocol ClientProxyProtocol {
func setAccountData<Content: Encodable>(content: Content, type: String) async -> Result<Void, ClientProxyError>
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
func logout() async
// swiftlint:disable:next function_parameter_count
func setPusher(pushkey: String,
kind: PusherKind?,
appId: String,
appDisplayName: String,
deviceDisplayName: String,
profileTag: String?,
lang: String,
url: String?,
format: PushFormat?,
defaultPayload: [AnyHashable: Any]?) async throws
}

View File

@ -53,15 +53,15 @@ struct MockClientProxy: ClientProxyProtocol {
.failure(.failedSettingAccountData)
}
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy {
.init(urlString: urlString)
}
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
throw ClientProxyError.failedLoadingMedia
}
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data {
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
throw ClientProxyError.failedLoadingMedia
}
@ -72,4 +72,18 @@ struct MockClientProxy: ClientProxyProtocol {
func logout() async {
// no-op
}
// swiftlint:disable:next function_parameter_count
func setPusher(pushkey: String,
kind: PusherKind?,
appId: String,
appDisplayName: String,
deviceDisplayName: String,
profileTag: String?,
lang: String,
url: String?,
format: PushFormat?,
defaultPayload: [AnyHashable: Any]?) async throws {
// no-op
}
}

View File

@ -17,11 +17,22 @@
import Foundation
import KeychainAccess
enum KeychainControllerService: String {
case sessions
case tests
var identifier: String {
InfoPlistReader.target.baseBundleIdentifier + "." + rawValue
}
}
class KeychainController: KeychainControllerProtocol {
private let keychain: Keychain
init(identifier: String) {
keychain = Keychain(service: identifier)
init(service: KeychainControllerService,
accessGroup: String) {
keychain = Keychain(service: service.identifier,
accessGroup: accessGroup)
}
func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) {

View File

@ -18,26 +18,26 @@ import Kingfisher
import UIKit
struct MediaProvider: MediaProviderProtocol {
private let clientProxy: ClientProxyProtocol
private let mediaProxy: MediaProxyProtocol
private let imageCache: Kingfisher.ImageCache
private let fileCache: FileCache
private let backgroundTaskService: BackgroundTaskServiceProtocol
private let backgroundTaskService: BackgroundTaskServiceProtocol?
init(clientProxy: ClientProxyProtocol,
init(mediaProxy: MediaProxyProtocol,
imageCache: Kingfisher.ImageCache,
fileCache: FileCache,
backgroundTaskService: BackgroundTaskServiceProtocol) {
self.clientProxy = clientProxy
backgroundTaskService: BackgroundTaskServiceProtocol?) {
self.mediaProxy = mediaProxy
self.imageCache = imageCache
self.fileCache = fileCache
self.backgroundTaskService = backgroundTaskService
}
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? {
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? {
guard let source else {
return nil
}
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), avatarSize: avatarSize)
let cacheKey = cacheKeyForURLString(source.url, avatarSize: avatarSize)
return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil)
}
@ -46,19 +46,19 @@ struct MediaProvider: MediaProviderProtocol {
return nil
}
return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), avatarSize: avatarSize)
return imageFromSource(.init(urlString: urlString), avatarSize: avatarSize)
}
func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), avatarSize: avatarSize)
await loadImageFromSource(.init(urlString: urlString), avatarSize: avatarSize)
}
func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
if let image = imageFromSource(source, avatarSize: avatarSize) {
return .success(image)
}
let loadImageBgTask = await backgroundTaskService.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)")
let loadImageBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)")
defer {
loadImageBgTask?.stop()
}
@ -73,9 +73,9 @@ struct MediaProvider: MediaProviderProtocol {
do {
let imageData: Data
if let avatarSize {
imageData = try await clientProxy.loadMediaThumbnailForSource(source.underlyingSource, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue))
imageData = try await mediaProxy.loadMediaThumbnailForSource(source, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue))
} else {
imageData = try await clientProxy.loadMediaContentForSource(source.underlyingSource)
imageData = try await mediaProxy.loadMediaContentForSource(source)
}
guard let image = UIImage(data: imageData) else {
@ -92,20 +92,20 @@ struct MediaProvider: MediaProviderProtocol {
}
}
func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL? {
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL? {
guard let source else {
return nil
}
let cacheKey = fileCacheKeyForURLString(source.underlyingSource.url())
let cacheKey = fileCacheKeyForURLString(source.url)
return fileCache.file(forKey: cacheKey, fileExtension: fileExtension)
}
@discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result<URL, MediaProviderError> {
@discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result<URL, MediaProviderError> {
if let url = fileFromSource(source, fileExtension: fileExtension) {
return .success(url)
}
let loadFileBgTask = await backgroundTaskService.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)")
let loadFileBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)")
defer {
loadFileBgTask?.stop()
}
@ -113,7 +113,7 @@ struct MediaProvider: MediaProviderProtocol {
let cacheKey = fileCacheKeyForURLString(source.url)
do {
let data = try await clientProxy.loadMediaContentForSource(source.underlyingSource)
let data = try await mediaProxy.loadMediaContentForSource(source)
let url = try fileCache.store(data, with: fileExtension, forKey: cacheKey)
return .success(url)
@ -128,12 +128,12 @@ struct MediaProvider: MediaProviderProtocol {
return nil
}
return fileFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)),
return fileFromSource(MediaSourceProxy(urlString: urlString),
fileExtension: fileExtension)
}
func loadFileFromURLString(_ urlString: String, fileExtension: String) async -> Result<URL, MediaProviderError> {
await loadFileFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)),
await loadFileFromSource(MediaSourceProxy(urlString: urlString),
fileExtension: fileExtension)
}

View File

@ -24,17 +24,17 @@ enum MediaProviderError: Error {
}
protocol MediaProviderProtocol {
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage?
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage?
@discardableResult func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
func imageFromURLString(_ urlString: String?, avatarSize: AvatarSize?) -> UIImage?
@discardableResult func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL?
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL?
@discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result<URL, MediaProviderError>
@discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result<URL, MediaProviderError>
func fileFromURLString(_ urlString: String?, fileExtension: String) -> URL?
@ -42,11 +42,11 @@ protocol MediaProviderProtocol {
}
extension MediaProviderProtocol {
func imageFromSource(_ source: MediaSource?) -> UIImage? {
func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? {
imageFromSource(source, avatarSize: nil)
}
@discardableResult func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy) async -> Result<UIImage, MediaProviderError> {
await loadImageFromSource(source, avatarSize: nil)
}

View 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)
}
}
}

View 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
}

View File

@ -17,24 +17,24 @@
import Foundation
import MatrixRustSDK
struct MediaSource: Equatable {
let underlyingSource: MatrixRustSDK.MediaSource
struct MediaSourceProxy: Equatable {
let underlyingSource: MediaSource
init(source: MediaSource) {
underlyingSource = source
}
init(urlString: String) {
underlyingSource = mediaSourceFromUrl(url: urlString)
}
var url: String {
underlyingSource.url()
}
init(source: MatrixRustSDK.MediaSource) {
underlyingSource = source
}
init(urlString: String) {
underlyingSource = MatrixRustSDK.mediaSourceFromUrl(url: urlString)
}
// MARK: - Equatable
static func == (lhs: MediaSource, rhs: MediaSource) -> Bool {
lhs.underlyingSource.url() == rhs.underlyingSource.url()
static func == (lhs: MediaSourceProxy, rhs: MediaSourceProxy) -> Bool {
lhs.url == rhs.url
}
}

View File

@ -18,11 +18,11 @@ import Foundation
import UIKit
struct MockMediaProvider: MediaProviderProtocol {
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? {
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? {
nil
}
func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
.failure(.failedRetrievingImage)
}
@ -50,11 +50,11 @@ struct MockMediaProvider: MediaProviderProtocol {
return .success(image)
}
func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL? {
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL? {
nil
}
@discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result<URL, MediaProviderError> {
@discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result<URL, MediaProviderError> {
.failure(.failedRetrievingFile)
}

View File

@ -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?) { }
}

View File

@ -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
}
}
}

View File

@ -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?)
}

View 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
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"
}
}

View File

@ -16,9 +16,8 @@
import Foundation
extension FileManager {
/// The URL of the primary app group container.
@objc var appGroupContainerURL: URL? {
containerURL(forSecurityApplicationGroupIdentifier: ElementInfoPlist.appGroupIdentifier)
class MockNotificationServiceProxy: NotificationServiceProxyProtocol {
func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy? {
nil
}
}

View File

@ -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
}
}

View File

@ -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)
// }
}
}

View 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 NotificationServiceProxyProtocol {
func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy?
}

View File

@ -113,8 +113,8 @@ extension MatrixRustSDK.ImageMessageContent: MessageContentProtocol { }
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.image`.
extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent {
var source: MediaSource {
MediaSource(source: content.source)
var source: MediaSourceProxy {
.init(source: content.source)
}
var width: CGFloat? {
@ -134,15 +134,15 @@ extension MatrixRustSDK.VideoMessageContent: MessageContentProtocol { }
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.video`.
extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent {
var source: MediaSource {
MediaSource(source: content.source)
var source: MediaSourceProxy {
.init(source: content.source)
}
var thumbnailSource: MediaSource? {
var thumbnailSource: MediaSourceProxy? {
guard let src = content.info?.thumbnailSource else {
return nil
}
return MediaSource(source: src)
return .init(source: src)
}
var duration: UInt64 {
@ -166,14 +166,14 @@ extension MatrixRustSDK.FileMessageContent: MessageContentProtocol { }
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.file`.
extension MessageTimelineItem where Content == MatrixRustSDK.FileMessageContent {
var source: MediaSource {
MediaSource(source: content.source)
var source: MediaSourceProxy {
.init(source: content.source)
}
var thumbnailSource: MediaSource? {
var thumbnailSource: MediaSourceProxy? {
guard let src = content.info?.thumbnailSource else {
return nil
}
return MediaSource(source: src)
return .init(source: src)
}
}

View File

@ -29,8 +29,8 @@ struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat
var senderDisplayName: String?
var senderAvatar: UIImage?
let source: MediaSource?
let thumbnailSource: MediaSource?
let source: MediaSourceProxy?
let thumbnailSource: MediaSourceProxy?
var cachedFileURL: URL?
var properties = RoomTimelineItemProperties()

View File

@ -29,7 +29,7 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
var senderDisplayName: String?
var senderAvatar: UIImage?
let source: MediaSource?
let source: MediaSourceProxy?
var image: UIImage?
var width: CGFloat?

View File

@ -30,8 +30,8 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
var senderAvatar: UIImage?
let duration: UInt64
let source: MediaSource?
let thumbnailSource: MediaSource?
let source: MediaSourceProxy?
let thumbnailSource: MediaSourceProxy?
var image: UIImage?
var cachedVideoURL: URL?

View File

@ -43,6 +43,16 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
stateMachine.processEvent(.start)
}
func stop() { }
func isDisplayingRoomScreen(withRoomId roomId: String) -> Bool {
stateMachine.isDisplayingRoomScreen(withRoomId: roomId)
}
func tryDisplayingRoomScreen(roomId: String) {
stateMachine.processEvent(.showRoomScreen(roomId: roomId))
}
// MARK: - Private
// swiftlint:disable:next cyclomatic_complexity

View File

@ -150,4 +150,14 @@ class UserSessionFlowCoordinatorStateMachine {
func addErrorHandler(_ handler: @escaping StateMachine<State, Event>.Handler) {
stateMachine.addErrorHandler(handler: handler)
}
/// Flag indicating the machine is displaying room screen with given room identifier
func isDisplayingRoomScreen(withRoomId roomId: String) -> Bool {
switch stateMachine.state {
case .roomScreen(let displayedRoomId):
return roomId == displayedRoomId
default:
return false
}
}
}

View File

@ -26,26 +26,14 @@ class UserSessionStore: UserSessionStoreProtocol {
var hasSessions: Bool { !keychainController.restorationTokens().isEmpty }
/// The base directory where all session data is stored.
private(set) lazy var baseDirectory: URL = {
guard let appGroupContainerURL = FileManager.default.appGroupContainerURL else {
fatalError("Should always be able to retrieve the container directory")
}
let baseDirectory: URL
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) {
keychainController = KeychainController(identifier: bundleIdentifier)
init(backgroundTaskService: BackgroundTaskServiceProtocol) {
keychainController = KeychainController(service: .sessions,
accessGroup: InfoPlistReader.target.appGroupIdentifier)
self.backgroundTaskService = backgroundTaskService
baseDirectory = .sessionsBaseDirectory
MXLog.debug("Setup base directory at: \(baseDirectory)")
}
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError> {
@ -58,7 +46,7 @@ class UserSessionStore: UserSessionStoreProtocol {
switch await restorePreviousLogin(credentials) {
case .success(let clientProxy):
return .success(UserSession(clientProxy: clientProxy,
mediaProvider: MediaProvider(clientProxy: clientProxy,
mediaProvider: MediaProvider(mediaProxy: clientProxy,
imageCache: .onlyInMemory,
fileCache: .default,
backgroundTaskService: backgroundTaskService)))
@ -77,7 +65,7 @@ class UserSessionStore: UserSessionStoreProtocol {
switch await setupProxyForClient(client) {
case .success(let clientProxy):
return .success(UserSession(clientProxy: clientProxy,
mediaProvider: MediaProvider(clientProxy: clientProxy,
mediaProvider: MediaProvider(mediaProxy: clientProxy,
imageCache: .onlyInMemory,
fileCache: .default,
backgroundTaskService: backgroundTaskService)))

View File

@ -17,8 +17,9 @@
import SwiftUI
import UIKit
class UITestsAppCoordinator: CoordinatorProtocol {
class UITestsAppCoordinator: AppCoordinatorProtocol {
private let navigationController: NavigationController
let notificationManager: NotificationManagerProtocol? = nil
init() {
navigationController = NavigationController()

View File

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>

View File

@ -22,6 +22,10 @@
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
@ -33,5 +37,7 @@
</array>
<key>appGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
<key>baseBundleIdentifier</key>
<string>$(BASE_BUNDLE_IDENTIFIER)</string>
</dict>
</plist>

View File

@ -46,15 +46,19 @@ targets:
UIInterfaceOrientationLandscapeRight
]
appGroupIdentifier: $(APP_GROUP_IDENTIFIER)
baseBundleIdentifier: $(BASE_BUNDLE_IDENTIFIER)
ITSAppUsesNonExemptEncryption: false
NSUserActivityTypes: [
INSendMessageIntent
]
settings:
base:
PRODUCT_NAME: ElementX
PRODUCT_BUNDLE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER)
MARKETING_VERSION: 1.0.9
CURRENT_PROJECT_VERSION: 1
DEVELOPMENT_TEAM: 7J4U792NQT
MARKETING_VERSION: $(MARKETING_VERSION)
CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION)
DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM)
CODE_SIGN_ENTITLEMENTS: ElementX/SupportingFiles/ElementX.entitlements
SWIFT_OBJC_BRIDGING_HEADER: ElementX/SupportingFiles/ElementX-Bridging-Header.h
SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h
@ -97,6 +101,7 @@ targets:
fi
dependencies:
- target: NSE
- package: MatrixRustSDK
- package: DesignKit
- package: AnalyticsEvents

View 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")
}
}

View 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
}
}

View 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)")
}
}

View 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
}
}

View 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
}
}
}

View 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
}
}

View 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>

View 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>

View 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

View File

@ -24,10 +24,3 @@ strings:
params:
enumName: ElementL10n
publicAccess: true
plist:
inputs: SupportingFiles/Info.plist
outputs:
templatePath: Templates/Plists/runtime-swift5-element-info.stencil
output: InfoPlist.swift
params:
enumName: ElementInfoPlist

View File

@ -70,8 +70,8 @@ targets:
basedOnDependencyAnalysis: false
shell: /bin/sh
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 'iPad (9th generation)' --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.1'
sources:
- path: ../Sources
@ -85,7 +85,8 @@ targets:
- path: ../../ElementX/Sources/UITests/UITestScreenIdentifier.swift
- path: ../../ElementX/Sources/Generated/Strings.swift
- path: ../../ElementX/Sources/Generated/Strings+Untranslated.swift
- path: ../../ElementX/Sources/Generated/InfoPlist.swift
- path: ../../ElementX/Resources
- 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

View File

@ -21,7 +21,8 @@ class KeychainControllerTests: XCTestCase {
var keychain: KeychainController!
override func setUp() {
keychain = KeychainController(identifier: "\(ElementInfoPlist.cfBundleIdentifier).tests")
keychain = KeychainController(service: .tests,
accessGroup: InfoPlistReader.target.appGroupIdentifier)
keychain.removeAllRestorationTokens()
}

View File

@ -25,11 +25,11 @@ class UserAgentBuilderTests: XCTestCase {
func testContainsClientName() {
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() {
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")
}
}

View File

@ -49,4 +49,4 @@ targets:
- path: ../SupportingFiles
- path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit
- path: ../Resources
- path: ../../ElementX/Sources/Other/InfoPlistReader.swift

1
changelog.d/243.feature Normal file
View File

@ -0,0 +1 @@
NSE: Configure target with commented code blocks.

View File

@ -25,12 +25,16 @@ settings:
BASE_APP_GROUP_IDENTIFIER: io.element
APP_GROUP_IDENTIFIER: group.$(BASE_APP_GROUP_IDENTIFIER)
BASE_BUNDLE_IDENTIFIER: io.element.elementx
MARKETING_VERSION: 1.0.9
CURRENT_PROJECT_VERSION: 1
DEVELOPMENT_TEAM: 7J4U792NQT
include:
- path: ElementX/SupportingFiles/target.yml
- path: UnitTests/SupportingFiles/target.yml
- path: UITests/SupportingFiles/target.yml
- path: IntegrationTests/SupportingFiles/target.yml
- path: NSE/SupportingFiles/target.yml
packages:
MatrixRustSDK: