mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-16 06:28:27 +02:00
Merge branch 'develop' into stefan/4881-configurable-app-name-localization-strings
# Conflicts: # Riot/Generated/Strings.swift
This commit is contained in:
@@ -63,6 +63,7 @@
|
||||
"switch" = "Switch";
|
||||
"more" = "More";
|
||||
"less" = "Less";
|
||||
"done" = "Done";
|
||||
|
||||
// Call Bar
|
||||
"callbar_only_single_active" = "Tap to return to the call (%@)";
|
||||
|
||||
@@ -1139,6 +1139,10 @@ public class VectorL10n: NSObject {
|
||||
public static var doNotAskAgain: String {
|
||||
return VectorL10n.tr("Vector", "do_not_ask_again")
|
||||
}
|
||||
/// Done
|
||||
public static var done: String {
|
||||
return VectorL10n.tr("Vector", "done")
|
||||
}
|
||||
/// %@ now supports end-to-end encryption but you need to log in again to enable it.\n\nYou can do it now or later from the application settings.
|
||||
public static func e2eEnablingOnAppUpdate(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "e2e_enabling_on_app_update", p1)
|
||||
|
||||
39
Riot/Managers/Logging/MatrixSDKLogger.swift
Normal file
39
Riot/Managers/Logging/MatrixSDKLogger.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
/**
|
||||
A logger for logging to MXLog.
|
||||
For use with UILog.
|
||||
*/
|
||||
class MatrixSDKLogger: LoggerProtocol {
|
||||
static func verbose(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
MXLog.verbose(message(), file, function, line: line, context: context)
|
||||
}
|
||||
static func debug(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
MXLog.debug(message(), file, function, line: line, context: context)
|
||||
}
|
||||
static func info(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
MXLog.info(message(), file, function, line: line, context: context)
|
||||
}
|
||||
static func warning(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
MXLog.warning(message(), file, function, line: line, context: context)
|
||||
}
|
||||
static func error(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
MXLog.error(message(), file, function, line: line, context: context)
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
|
||||
// MARK: - Public methods
|
||||
|
||||
func start() {
|
||||
self.setupLogger()
|
||||
self.setupTheme()
|
||||
|
||||
if BuildSettings.enableSideMenu {
|
||||
@@ -100,6 +101,9 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private func setupLogger() {
|
||||
UILog.configure(logger: MatrixSDKLogger.self)
|
||||
}
|
||||
|
||||
private func setupTheme() {
|
||||
ThemeService.shared().themeId = RiotSettings.shared.userInterfaceTheme
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
|
||||
/// AvatarViewDataProtocol describe a view data that should be given to an AvatarView sublcass
|
||||
protocol AvatarViewDataProtocol: AvatarType {
|
||||
protocol AvatarViewDataProtocol: AvatarProtocol {
|
||||
/// Matrix item identifier (user id or room id)
|
||||
var matrixItemId: String { get }
|
||||
|
||||
|
||||
@@ -631,7 +631,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
self.updateRoomReadMarker = NO;
|
||||
isAppeared = NO;
|
||||
|
||||
[VoiceMessageMediaServiceProvider.sharedProvider stopAllServices];
|
||||
[VoiceMessageMediaServiceProvider.sharedProvider pauseAllServices];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
|
||||
@@ -31,7 +31,13 @@ import MediaPlayer
|
||||
|
||||
private var displayLink: CADisplayLink!
|
||||
|
||||
// Retain currently playing audio player so it doesn't stop playing on timeline cell reuse
|
||||
|
||||
|
||||
// Retain active audio players(playing or paused) so it doesn't stop playing on timeline cell reuse
|
||||
// and we can pause/resume players on switching rooms.
|
||||
private var activeAudioPlayers: Set<VoiceMessageAudioPlayer>
|
||||
|
||||
// Keep reference to currently playing player for remote control.
|
||||
private var currentlyPlayingAudioPlayer: VoiceMessageAudioPlayer?
|
||||
|
||||
@objc public static let sharedProvider = VoiceMessageMediaServiceProvider()
|
||||
@@ -87,7 +93,7 @@ import MediaPlayer
|
||||
private override init() {
|
||||
audioPlayers = NSMapTable<NSString, VoiceMessageAudioPlayer>(valueOptions: .weakMemory)
|
||||
audioRecorders = NSHashTable<VoiceMessageAudioRecorder>(options: .weakMemory)
|
||||
|
||||
activeAudioPlayers = Set<VoiceMessageAudioPlayer>()
|
||||
super.init()
|
||||
|
||||
displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector)
|
||||
@@ -113,16 +119,17 @@ import MediaPlayer
|
||||
return audioRecorder
|
||||
}
|
||||
|
||||
@objc func stopAllServices() {
|
||||
stopAllServicesExcept(nil)
|
||||
@objc func pauseAllServices() {
|
||||
pauseAllServicesExcept(nil)
|
||||
}
|
||||
|
||||
// MARK: - VoiceMessageAudioPlayerDelegate
|
||||
|
||||
func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
||||
currentlyPlayingAudioPlayer = audioPlayer
|
||||
activeAudioPlayers.insert(audioPlayer)
|
||||
setUpRemoteCommandCenter()
|
||||
stopAllServicesExcept(audioPlayer)
|
||||
pauseAllServicesExcept(audioPlayer)
|
||||
}
|
||||
|
||||
func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
||||
@@ -130,6 +137,7 @@ import MediaPlayer
|
||||
currentlyPlayingAudioPlayer = nil
|
||||
tearDownRemoteCommandCenter()
|
||||
}
|
||||
activeAudioPlayers.remove(audioPlayer)
|
||||
}
|
||||
|
||||
func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
||||
@@ -137,17 +145,18 @@ import MediaPlayer
|
||||
currentlyPlayingAudioPlayer = nil
|
||||
tearDownRemoteCommandCenter()
|
||||
}
|
||||
activeAudioPlayers.remove(audioPlayer)
|
||||
}
|
||||
|
||||
// MARK: - VoiceMessageAudioRecorderDelegate
|
||||
|
||||
func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) {
|
||||
stopAllServicesExcept(audioRecorder)
|
||||
pauseAllServicesExcept(audioRecorder)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func stopAllServicesExcept(_ service: AnyObject?) {
|
||||
private func pauseAllServicesExcept(_ service: AnyObject?) {
|
||||
for audioRecorder in audioRecorders.allObjects {
|
||||
if audioRecorder === service {
|
||||
continue
|
||||
@@ -165,8 +174,7 @@ import MediaPlayer
|
||||
continue
|
||||
}
|
||||
|
||||
audioPlayer.stop()
|
||||
audioPlayer.unloadContent()
|
||||
audioPlayer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ targets:
|
||||
# Riot will provide it's own LocaleProviderType so exclude.
|
||||
excludes:
|
||||
- "Common/Locale/LocaleProvider.swift"
|
||||
- "**/Test/**"
|
||||
- path: ../Tools
|
||||
excludes:
|
||||
- "Logs"
|
||||
|
||||
@@ -2,6 +2,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>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
||||
@@ -16,10 +16,8 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
A visual cue to user that something is in progress.
|
||||
*/
|
||||
@available(iOS 14.0, *)
|
||||
/// A visual cue to user that something is in progress.
|
||||
struct ActivityIndicator: View {
|
||||
|
||||
private enum Constants {
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
A modifier for showing the activity indcator centered over a view.
|
||||
*/
|
||||
@available(iOS 14.0, *)
|
||||
/// A modifier for showing the activity indicator centered over a view.
|
||||
struct ActivityIndicatorModifier: ViewModifier {
|
||||
var show: Bool
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol AvatarInputType: AvatarType {
|
||||
protocol AvatarInputProtocol: AvatarProtocol {
|
||||
var mxContentUri: String? { get }
|
||||
var matrixItemId: String { get }
|
||||
var displayName: String? { get }
|
||||
}
|
||||
|
||||
struct AvatarInput: AvatarInputType {
|
||||
struct AvatarInput: AvatarInputProtocol {
|
||||
let mxContentUri: String?
|
||||
var matrixItemId: String
|
||||
let displayName: String?
|
||||
@@ -16,4 +16,4 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol AvatarType { }
|
||||
protocol AvatarProtocol { }
|
||||
33
RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift
Normal file
33
RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
/// A protocol that any class or struct can conform to
|
||||
/// so that it can easily produce avatar data.
|
||||
///
|
||||
/// E.g. MXRoom, MxUser can conform to this making it
|
||||
/// easy to grab the avatar data for display.
|
||||
protocol Avatarable: AvatarInputProtocol { }
|
||||
extension Avatarable {
|
||||
var avatarData: AvatarInput {
|
||||
AvatarInput(
|
||||
mxContentUri: mxContentUri,
|
||||
matrixItemId: matrixItemId,
|
||||
displayName: displayName
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ enum AvatarServiceError: Error {
|
||||
case loadingImageFailed(Error?)
|
||||
}
|
||||
|
||||
class AvatarService: AvatarServiceType {
|
||||
class AvatarService: AvatarServiceProtocol {
|
||||
|
||||
private enum Constants {
|
||||
static let mimeType = "image/jpeg"
|
||||
@@ -33,18 +33,21 @@ class AvatarService: AvatarServiceType {
|
||||
|
||||
private let mediaManager: MXMediaManager
|
||||
|
||||
static func instantiate(mediaManager: MXMediaManager) -> AvatarServiceProtocol {
|
||||
return AvatarService(mediaManager: mediaManager)
|
||||
}
|
||||
|
||||
init(mediaManager: MXMediaManager) {
|
||||
self.mediaManager = mediaManager
|
||||
}
|
||||
|
||||
/**
|
||||
Given an mxContentUri, this function returns a Future of UIImage.
|
||||
If possible it will retrieve the image from network or cache, otherwise it will error.
|
||||
|
||||
- Parameter mxContentUri: matrix uri of the avatar to fetch
|
||||
- Parameter avatarSize: The size of avatar to retrieve as defined in the DesignKit spec.
|
||||
- Returns: A Future of UIImage that returns an error if it fails to fetch the image
|
||||
*/
|
||||
/// Given an mxContentUri, this function returns a Future of UIImage.
|
||||
///
|
||||
/// If possible it will retrieve the image from network or cache, otherwise it will error.
|
||||
/// - Parameters:
|
||||
/// - mxContentUri: matrix uri of the avatar to fetch
|
||||
/// - avatarSize: The size of avatar to retrieve as defined in the DesignKit spec.
|
||||
/// - Returns: A Future of UIImage that returns an error if it fails to fetch the image.
|
||||
@available(iOS 14.0, *)
|
||||
func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future<UIImage, Error> {
|
||||
|
||||
@@ -20,8 +20,8 @@ import DesignKit
|
||||
import UIKit
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class MockAvatarService: AvatarServiceType {
|
||||
static let example: AvatarServiceType = MockAvatarService()
|
||||
class MockAvatarService: AvatarServiceProtocol {
|
||||
static let example: AvatarServiceProtocol = MockAvatarService()
|
||||
func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future<UIImage, Error> {
|
||||
Future { promise in
|
||||
promise(.success(Asset.Images.appSymbol.image))
|
||||
@@ -66,7 +66,7 @@ struct AvatarImage: View {
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension AvatarImage {
|
||||
init(avatarData: AvatarInputType, size: AvatarSize) {
|
||||
init(avatarData: AvatarInputProtocol, size: AvatarSize) {
|
||||
self.init(
|
||||
mxContentUri: avatarData.mxContentUri,
|
||||
matrixItemId: avatarData.matrixItemId,
|
||||
|
||||
@@ -19,10 +19,9 @@ import DesignKit
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
Provides a simple api to retrieve and cache avatar images
|
||||
*/
|
||||
protocol AvatarServiceType {
|
||||
|
||||
/// Provides a simple api to retrieve and cache avatar images
|
||||
protocol AvatarServiceProtocol {
|
||||
@available(iOS 14.0, *)
|
||||
func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future<UIImage, Error>
|
||||
}
|
||||
@@ -18,19 +18,23 @@ import Foundation
|
||||
import Combine
|
||||
import DesignKit
|
||||
|
||||
/**
|
||||
Simple ViewModel that supports loading an avatar image of a particular size
|
||||
as specified in DesignKit and delivering the UIImage to the UI if possible.
|
||||
*/
|
||||
@available(iOS 14.0, *)
|
||||
/// Simple ViewModel that supports loading an avatar image
|
||||
class AvatarViewModel: InjectableObject, ObservableObject {
|
||||
|
||||
@Inject var avatarService: AvatarServiceType
|
||||
@Inject var avatarService: AvatarServiceProtocol
|
||||
|
||||
@Published private(set) var viewState = AvatarViewState.empty
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
/// Load an avatar
|
||||
/// - Parameters:
|
||||
/// - mxContentUri: The matrix content URI of the avatar.
|
||||
/// - matrixItemId: The id of the matrix item represented by the avatar.
|
||||
/// - displayName: Display name of the avatar.
|
||||
/// - colorCount: The count of total avatar colors used to generate the stable color index.
|
||||
/// - avatarSize: The size of the avatar to fetch (as defined within DesignKit).
|
||||
func loadAvatar(
|
||||
mxContentUri: String?,
|
||||
matrixItemId: String,
|
||||
@@ -47,17 +51,16 @@ class AvatarViewModel: InjectableObject, ObservableObject {
|
||||
avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize)
|
||||
.sink { completion in
|
||||
guard case let .failure(error) = completion else { return }
|
||||
// MXLog.error("[AvatarService] Failed to retrieve avatar: \(error)")
|
||||
// TODO: Report non-fatal error when we have Sentry or similar.
|
||||
UILog.error("[AvatarService] Failed to retrieve avatar: \(error)")
|
||||
} receiveValue: { image in
|
||||
self.viewState = .avatar(image)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the first character of a string capialized or else an empty string.
|
||||
*/
|
||||
/// Get the first character of a string capialized or else an empty string.
|
||||
/// - Parameter string: The input string to get the capitalized letter from.
|
||||
/// - Returns: The capitalized first letter.
|
||||
private func firstCharacterCapitalized(_ string: String?) -> String {
|
||||
guard let character = string?.first else {
|
||||
return ""
|
||||
@@ -65,10 +68,13 @@ class AvatarViewModel: InjectableObject, ObservableObject {
|
||||
return String(character).capitalized
|
||||
}
|
||||
|
||||
/**
|
||||
Provides the same color each time for a specified matrixId.
|
||||
Same algorithm as in AvatarGenerator.
|
||||
*/
|
||||
/// Provides the same color each time for a specified matrixId
|
||||
///
|
||||
/// Same algorithm as in AvatarGenerator.
|
||||
/// - Parameters:
|
||||
/// - matrixItemId: the matrix id used as input to create the stable index.
|
||||
/// - colorCount: The number of total colors we want to index in to.
|
||||
/// - Returns: The stable index.
|
||||
private func stableColorIndex(matrixItemId: String, colorCount: Int) -> Int {
|
||||
// Sum all characters
|
||||
let sum = matrixItemId.utf8
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
A Modifier to be called from the top-most SwiftUI view before being added to a HostViewController
|
||||
Provides any app level configuration the SwiftUI hierarchy might need (E.g. to monitor theme changes).
|
||||
*/
|
||||
/// A Modifier to be called from the top-most SwiftUI view before being added to a HostViewController.
|
||||
///
|
||||
/// Provides any app level configuration the SwiftUI hierarchy might need (E.g. to monitor theme changes).
|
||||
@available(iOS 14.0, *)
|
||||
struct VectorContentModifier: ViewModifier {
|
||||
|
||||
|
||||
@@ -16,19 +16,18 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Used for storing and resolving dependencies at runtime.
|
||||
*/
|
||||
/// Used for storing and resolving dependencies at runtime.
|
||||
struct DependencyContainer {
|
||||
|
||||
// Stores the dependencies with type information removed.
|
||||
private var dependencyStore: [String: Any] = [:]
|
||||
|
||||
/**
|
||||
Resolve a dependency by type.
|
||||
Given a particlar `Type` (Inferred from return type),
|
||||
generate a key and retrieve from storage.
|
||||
*/
|
||||
/// Resolve a dependency by type.
|
||||
///
|
||||
/// Given a particular `Type` (Inferred from return type),
|
||||
/// generate a key and retrieve from storage.
|
||||
///
|
||||
/// - Returns: The resolved dependency.
|
||||
func resolve<T>() -> T {
|
||||
let key = String(describing: T.self)
|
||||
guard let t = dependencyStore[key] as? T else {
|
||||
@@ -37,10 +36,10 @@ struct DependencyContainer {
|
||||
return t
|
||||
}
|
||||
|
||||
/**
|
||||
Register a dependency.
|
||||
Given a dependency, generate a key from it's `Type` and save in storage.
|
||||
*/
|
||||
/// Register a dependency.
|
||||
///
|
||||
/// Given a dependency, generate a key from it's `Type` and save in storage.
|
||||
/// - Parameter dependency: The dependency to register.
|
||||
mutating func register<T>(dependency: T) {
|
||||
let key = String(describing: T.self)
|
||||
dependencyStore[key] = dependency
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
An Environment Key for retrieving runtime dependencies to be injected into `ObservableObjects`
|
||||
that are owned by a View (i.e. `@StateObject`'s, such as ViewModels owned by the View).
|
||||
*/
|
||||
/// An Environment Key for retrieving runtime dependencies.
|
||||
///
|
||||
/// Dependencies are to be injected into `ObservableObjects`
|
||||
/// that are owned by a View (i.e. `@StateObject`'s, such as ViewModels owned by the View).
|
||||
private struct DependencyContainerKey: EnvironmentKey {
|
||||
static let defaultValue = DependencyContainer()
|
||||
}
|
||||
@@ -36,12 +36,13 @@ extension EnvironmentValues {
|
||||
@available(iOS 14.0, *)
|
||||
extension View {
|
||||
|
||||
/**
|
||||
A modifier for adding a dependency to the SwiftUI view hierarchy's dependency container.
|
||||
Important: When adding a dependency to cast it to the type in which it will be injected.
|
||||
So if adding `MockDependency` but type at injection is `Dependency` remember to cast
|
||||
to `Dependency` first.
|
||||
*/
|
||||
/// A modifier for adding a dependency to the SwiftUI view hierarchy's dependency container.
|
||||
///
|
||||
/// Important: When adding a dependency to cast it to the type in which it will be injected.
|
||||
/// So if adding `MockDependency` but type at injection is `Dependency` remember to cast
|
||||
/// to `Dependency` first.
|
||||
/// - Parameter dependency: The dependency to add.
|
||||
/// - Returns: The wrapped view that now includes the dependency.
|
||||
func addDependency<T>(_ dependency: T) -> some View {
|
||||
transformEnvironment(\.dependencies) { container in
|
||||
container.register(dependency: dependency)
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A property wrapped used to inject from the dependency
|
||||
container on the instance to instance properties.
|
||||
E.g. ```@Inject var someClass: SomeClass```
|
||||
*/
|
||||
/// A property wrapped used to inject from the dependency container on the instance, to instance properties.
|
||||
///
|
||||
/// ```
|
||||
/// @Inject var someClass: SomeClass
|
||||
/// ```
|
||||
@propertyWrapper struct Inject<Value> {
|
||||
|
||||
static subscript<T: Injectable>(
|
||||
|
||||
@@ -16,18 +16,16 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A protocol for classes that can be injected with a dependency container
|
||||
*/
|
||||
/// A protocol for classes that can be injected with a dependency container
|
||||
protocol Injectable: AnyObject {
|
||||
var dependencies: DependencyContainer! { get set }
|
||||
}
|
||||
|
||||
|
||||
extension Injectable {
|
||||
/**
|
||||
Used to inject the dependency container into an Injectable.
|
||||
*/
|
||||
|
||||
/// Used to inject the dependency container into an Injectable.
|
||||
/// - Parameter dependencies: The `DependencyContainer` to inject.
|
||||
func inject(dependencies: DependencyContainer) {
|
||||
self.dependencies = dependencies
|
||||
}
|
||||
|
||||
@@ -16,10 +16,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Class that can be extended and supports
|
||||
injection and the `@Inject` property wrapper.
|
||||
*/
|
||||
/// Class that can be extended that supports injection and the `@Inject` property wrapper.
|
||||
open class InjectableObject: Injectable {
|
||||
var dependencies: DependencyContainer!
|
||||
}
|
||||
|
||||
@@ -17,13 +17,11 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/**
|
||||
Sams as `assign(to:on:)` but maintains a weak reference to object(Useful in cases where you want to pass self and not cause a retain cycle.)
|
||||
- SeeAlso:
|
||||
[assign(to:on:)](https://developer.apple.com/documentation/combine/just/assign(to:on:))
|
||||
*/
|
||||
@available(iOS 14.0, *)
|
||||
extension Publisher where Failure == Never {
|
||||
/// Same as `assign(to:on:)` but maintains a weak reference to object
|
||||
///
|
||||
/// Useful in cases where you want to pass self and not cause a retain cycle.
|
||||
func weakAssign<T: AnyObject>(
|
||||
to keyPath: ReferenceWritableKeyPath<T, Output>,
|
||||
on object: T
|
||||
|
||||
26
RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift
Normal file
26
RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
/// A logger protocol that enables conforming types to be used with UILog.
|
||||
protocol LoggerProtocol {
|
||||
static func verbose(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?)
|
||||
static func debug(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?)
|
||||
static func info(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?)
|
||||
static func warning(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?)
|
||||
static func error(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?)
|
||||
}
|
||||
38
RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift
Normal file
38
RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
/// A logger for logging to `print`.
|
||||
///
|
||||
/// For use with UILog.
|
||||
class PrintLogger: LoggerProtocol {
|
||||
static func verbose(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
print(message())
|
||||
}
|
||||
static func debug(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
print(message())
|
||||
}
|
||||
static func info(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
print(message())
|
||||
}
|
||||
static func warning(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
print(message())
|
||||
}
|
||||
static func error(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
print(message())
|
||||
}
|
||||
}
|
||||
73
RiotSwiftUI/Modules/Common/Logging/UILog.swift
Normal file
73
RiotSwiftUI/Modules/Common/Logging/UILog.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
/// A logger for use in different application targets.
|
||||
///
|
||||
/// It can be configured at runtime with a suitable logger.
|
||||
class UILog: LoggerProtocol {
|
||||
|
||||
static var _logger: LoggerProtocol.Type?
|
||||
static func configure(logger: LoggerProtocol.Type) {
|
||||
_logger = logger
|
||||
}
|
||||
|
||||
static func verbose(
|
||||
_ message: @autoclosure () -> Any,
|
||||
_ file: String = #file,
|
||||
_ function: String = #function,
|
||||
line: Int = #line,
|
||||
context: Any? = nil) {
|
||||
_logger?.verbose(message(), file, function, line: line, context: context)
|
||||
}
|
||||
|
||||
static func debug(
|
||||
_ message: @autoclosure () -> Any,
|
||||
_ file: String = #file,
|
||||
_ function: String = #function,
|
||||
line: Int = #line,
|
||||
context: Any? = nil) {
|
||||
_logger?.debug(message(), file, function, line: line, context: context)
|
||||
}
|
||||
|
||||
static func info(
|
||||
_ message: @autoclosure () -> Any,
|
||||
_ file: String = #file,
|
||||
_ function: String = #function,
|
||||
line: Int = #line,
|
||||
context: Any? = nil) {
|
||||
_logger?.info(message(), file, function, line: line, context: context)
|
||||
}
|
||||
|
||||
static func warning(
|
||||
_ message: @autoclosure () -> Any,
|
||||
_ file: String = #file,
|
||||
_ function: String = #function,
|
||||
line: Int = #line,
|
||||
context: Any? = nil) {
|
||||
_logger?.warning(message(), file, function, line: line, context: context)
|
||||
}
|
||||
|
||||
static func error(
|
||||
_ message: @autoclosure () -> Any,
|
||||
_ file: String = #file,
|
||||
_ function: String = #function,
|
||||
line: Int = #line,
|
||||
context: Any? = nil) {
|
||||
_logger?.error(message(), file, function, line: line, context: context)
|
||||
}
|
||||
}
|
||||
24
RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
Normal file
24
RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
/// The static list of mocked screens in RiotSwiftUI
|
||||
@available(iOS 14.0, *)
|
||||
enum MockAppScreens {
|
||||
static let appScreens = [MockTemplateUserProfileScreenState.self]
|
||||
}
|
||||
|
||||
79
RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift
Normal file
79
RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
/// Used for mocking top level screens and their various states.
|
||||
@available(iOS 14.0, *)
|
||||
protocol MockScreenState {
|
||||
static var screenStates: [MockScreenState] { get }
|
||||
var screenType: Any.Type { get }
|
||||
var screenView: AnyView { get }
|
||||
var stateTitle: String { get }
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MockScreenState {
|
||||
|
||||
/// Get a list of the screens for every screen state.
|
||||
static var screensViews: [AnyView] {
|
||||
screenStates.map(\.screenView)
|
||||
}
|
||||
|
||||
/// A unique key to identify each screen state.
|
||||
static var screenStateKeys: [String] {
|
||||
return Array(0..<screenStates.count).map(String.init)
|
||||
}
|
||||
|
||||
/// Render each of the screen states in a group applying
|
||||
/// any optional environment variables.
|
||||
/// - Parameters:
|
||||
/// - themeId: id of theme to render the screens with
|
||||
/// - locale: Locale to render the screens with
|
||||
/// - sizeCategory: type sizeCategory to render the screens with
|
||||
/// - Returns: The group of screens
|
||||
static func screenGroup(
|
||||
themeId: ThemeIdentifier = .light,
|
||||
locale: Locale = Locale.current,
|
||||
sizeCategory: ContentSizeCategory = ContentSizeCategory.medium
|
||||
) -> some View {
|
||||
Group {
|
||||
ForEach(0..<screensViews.count) { index in
|
||||
screensViews[index]
|
||||
}
|
||||
}
|
||||
.theme(themeId)
|
||||
.environment(\.locale, locale)
|
||||
.environment(\.sizeCategory, sizeCategory)
|
||||
}
|
||||
|
||||
/// A title to represent the screen and it's screen state
|
||||
var screenTitle: String {
|
||||
"\(String(describing: screenType.self)): \(stateTitle)"
|
||||
}
|
||||
|
||||
/// A title to represent this screen state
|
||||
var stateTitle: String {
|
||||
String(describing: self)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MockScreenState where Self: CaseIterable {
|
||||
static var screenStates: [MockScreenState] {
|
||||
return Array(self.allCases)
|
||||
}
|
||||
}
|
||||
49
RiotSwiftUI/Modules/Common/Mock/ScreenList.swift
Normal file
49
RiotSwiftUI/Modules/Common/Mock/ScreenList.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct ScreenList: View {
|
||||
|
||||
private var allStates: [MockScreenState]
|
||||
|
||||
init(screens: [MockScreenState.Type]) {
|
||||
self.allStates = screens.flatMap{ $0.screenStates }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
ForEach(0..<allStates.count) { i in
|
||||
let state = allStates[i]
|
||||
NavigationLink(destination: state.screenView) {
|
||||
Text(state.screenTitle)
|
||||
.accessibilityIdentifier(String(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Screen States")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct ScreenList_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ScreenList(screens: [MockTemplateUserProfileScreenState.self])
|
||||
}
|
||||
}
|
||||
75
RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift
Normal file
75
RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// Copyright 2021 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 XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
/// XCTestCase subclass to ease testing of `MockScreenState`.
|
||||
/// Creates a test case for each screen state, launches the app,
|
||||
/// goes to the correct screen and provides the state and key for each
|
||||
/// invocation of the test.
|
||||
@available(iOS 14.0, *)
|
||||
class MockScreenTest: XCTestCase {
|
||||
|
||||
enum Constants {
|
||||
static let defaultTimeout: TimeInterval = 3
|
||||
}
|
||||
|
||||
class var screenType: MockScreenState.Type? {
|
||||
return nil
|
||||
}
|
||||
|
||||
class func createTest() -> MockScreenTest {
|
||||
return MockScreenTest()
|
||||
}
|
||||
|
||||
var screenState: MockScreenState?
|
||||
var screenStateKey: String?
|
||||
let app = XCUIApplication()
|
||||
|
||||
override class var defaultTestSuite: XCTestSuite {
|
||||
let testSuite = XCTestSuite(name: NSStringFromClass(self))
|
||||
guard let screenType = screenType else {
|
||||
return testSuite
|
||||
}
|
||||
// Create a test case for each screen state
|
||||
screenType.screenStates.enumerated().forEach { index, screenState in
|
||||
let key = screenType.screenStateKeys[index]
|
||||
addTestFor(screenState: screenState, screenStateKey: key, toTestSuite: testSuite)
|
||||
}
|
||||
return testSuite
|
||||
}
|
||||
|
||||
class func addTestFor(screenState: MockScreenState, screenStateKey: String, toTestSuite testSuite: XCTestSuite) {
|
||||
let test = createTest()
|
||||
test.screenState = screenState
|
||||
test.screenStateKey = screenStateKey
|
||||
testSuite.addTest(test)
|
||||
}
|
||||
|
||||
open override func setUpWithError() throws {
|
||||
// For every test case launch the app and go to the relevant screen
|
||||
continueAfterFailure = false
|
||||
app.launch()
|
||||
goToScreen()
|
||||
}
|
||||
|
||||
private func goToScreen() {
|
||||
guard let screenKey = screenStateKey else { fatalError("no screen") }
|
||||
let link = app.buttons[screenKey]
|
||||
link.tap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Copyright 2021 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 XCTest
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension XCTestCase {
|
||||
/// XCTest utility to wait for results from publishers, so that the output can be used for assertions.
|
||||
///
|
||||
/// ```
|
||||
/// let collectedEvents = somePublisher.collect(3).first()
|
||||
/// XCTAssertEqual(try xcAwait(collectedEvents), [expected, values, here])
|
||||
/// ```
|
||||
/// - Parameters:
|
||||
/// - publisher: The publisher to wait on.
|
||||
/// - timeout: A timeout after which we give up.
|
||||
/// - Throws: If it can't get the unwrapped result.
|
||||
/// - Returns: The unwrapped result.
|
||||
func xcAwait<T: Publisher>(
|
||||
_ publisher: T,
|
||||
timeout: TimeInterval = 10
|
||||
) throws -> T.Output {
|
||||
return try xcAwaitDeferred(publisher, timeout: timeout)()
|
||||
}
|
||||
|
||||
/// XCTest utility that allows for a deferred wait of results from publishers, so that the output can be used for assertions.
|
||||
///
|
||||
/// ```
|
||||
/// let collectedEvents = somePublisher.collect(3).first()
|
||||
/// let awaitDeferred = xcAwaitDeferred(collectedEvents)
|
||||
/// // Do some other work that publishes to somePublisher
|
||||
/// XCTAssertEqual(try awaitDeferred(), [expected, values, here])
|
||||
/// ```
|
||||
/// - Parameters:
|
||||
/// - publisher: The publisher to wait on.
|
||||
/// - timeout: A timeout after which we give up.
|
||||
/// - Returns: A closure that starts the waiting of results when called. The closure will return the unwrapped result.
|
||||
func xcAwaitDeferred<T: Publisher>(
|
||||
_ publisher: T,
|
||||
timeout: TimeInterval = 10
|
||||
) -> (() throws -> (T.Output)) {
|
||||
var result: Result<T.Output, Error>?
|
||||
let expectation = self.expectation(description: "Awaiting publisher")
|
||||
|
||||
let cancellable = publisher.sink(
|
||||
receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
result = .failure(error)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
|
||||
expectation.fulfill()
|
||||
},
|
||||
receiveValue: { value in
|
||||
result = .success(value)
|
||||
}
|
||||
)
|
||||
return {
|
||||
self.waitForExpectations(timeout: timeout)
|
||||
cancellable.cancel()
|
||||
let unwrappedResult = try XCTUnwrap(
|
||||
result,
|
||||
"Awaited publisher did not produce any output"
|
||||
)
|
||||
return try unwrappedResult.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,11 @@
|
||||
import Foundation
|
||||
import DesignKit
|
||||
|
||||
/**
|
||||
Extension to `ThemeIdentifier` for getting the SwiftUI theme.
|
||||
*/
|
||||
@available(iOS 14.0, *)
|
||||
extension ThemeIdentifier {
|
||||
fileprivate static let defaultTheme = DefaultThemeSwiftUI()
|
||||
fileprivate static let darkTheme = DarkThemeSwiftUI()
|
||||
/// Extension to `ThemeIdentifier` for getting the SwiftUI theme.
|
||||
public var themeSwiftUI: ThemeSwiftUI {
|
||||
switch self {
|
||||
case .light:
|
||||
|
||||
@@ -31,25 +31,21 @@ extension EnvironmentValues {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A theme modifier for setting the theme for this view and all its descendants in the hierarchy.
|
||||
- Parameters:
|
||||
- theme: a Theme to be set as the environment value.
|
||||
*/
|
||||
@available(iOS 14.0, *)
|
||||
extension View {
|
||||
/// A theme modifier for setting the theme for this view and all its descendants in the hierarchy.
|
||||
/// - Parameter theme: A theme to be set as the environment value.
|
||||
/// - Returns: The target view with the theme applied.
|
||||
func theme(_ theme: ThemeSwiftUI) -> some View {
|
||||
environment(\.theme, theme)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A theme modifier for setting the theme by id for this view and all its descendants in the hierarchy.
|
||||
- Parameters:
|
||||
- themeId: ThemeIdentifier of a theme to be set as the environment value.
|
||||
*/
|
||||
@available(iOS 14.0, *)
|
||||
extension View {
|
||||
/// A theme modifier for setting the theme by id for this view and all its descendants in the hierarchy.
|
||||
/// - Parameter themeId: ThemeIdentifier of a theme to be set as the environment value.
|
||||
/// - Returns: The target view with the theme applied.
|
||||
func theme(_ themeId: ThemeIdentifier) -> some View {
|
||||
return environment(\.theme, themeId.themeSwiftUI)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,10 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/**
|
||||
Provides the theme and theme updates to SwiftUI.
|
||||
Replaces the old ThemeObserver. Riot app can push updates to this class
|
||||
removing the dependency of this class on the `ThemeService`.
|
||||
*/
|
||||
/// Provides the theme and theme updates to SwiftUI.
|
||||
///
|
||||
/// Replaces the old ThemeObserver. Riot app can push updates to this class
|
||||
/// removing the dependency of this class on the `ThemeService`.
|
||||
@available(iOS 14.0, *)
|
||||
class ThemePublisher: ObservableObject {
|
||||
|
||||
|
||||
@@ -17,18 +17,15 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
Used to calculate the frame of a view. Useful in situations as with `ZStack` where
|
||||
you might want to layout views using alignment guides.
|
||||
Example usage:
|
||||
```
|
||||
@State private var frame: CGRect = CGRect.zero
|
||||
...
|
||||
SomeView()
|
||||
.background(ViewFrameReader(frame: $frame))
|
||||
|
||||
```
|
||||
*/
|
||||
/// Used to calculate the frame of a view.
|
||||
///
|
||||
/// Useful in situations as with `ZStack` where you might want to layout views using alignment guides.
|
||||
/// ```
|
||||
/// @State private var frame: CGRect = CGRect.zero
|
||||
/// ...
|
||||
/// SomeView()
|
||||
/// .background(ViewFrameReader(frame: $frame))
|
||||
/// ```
|
||||
@available(iOS 14.0, *)
|
||||
struct ViewFrameReader: View {
|
||||
@Binding var frame: CGRect
|
||||
|
||||
37
RiotSwiftUI/Modules/Common/ViewModel/BindableState.swift
Normal file
37
RiotSwiftUI/Modules/Common/ViewModel/BindableState.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
/// Represents a specific portion of the ViewState that can be bound to with SwiftUI's [2-way binding](https://developer.apple.com/documentation/swiftui/binding).
|
||||
protocol BindableState {
|
||||
/// The associated type of the Bindable State. Defaults to Void.
|
||||
associatedtype BindStateType = Void
|
||||
var bindings: BindStateType { get set }
|
||||
}
|
||||
|
||||
extension BindableState where BindStateType == Void {
|
||||
/// We provide a default implementation for the Void type so that we can have `ViewState` that
|
||||
/// just doesn't include/take advantage of the bindings.
|
||||
var bindings: Void {
|
||||
get {
|
||||
()
|
||||
}
|
||||
set {
|
||||
fatalError("Can't bind to the default Void binding.")
|
||||
}
|
||||
}
|
||||
}
|
||||
143
RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift
Normal file
143
RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift
Normal file
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// Copyright 2021 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 Combine
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
|
||||
/// A constrained and concise interface for interacting with the ViewModel.
|
||||
///
|
||||
/// This class is closely bound to`StateStoreViewModel`. It provides the exact interface the view should need to interact
|
||||
/// ViewModel (as modelled on our previous template architecture with the addition of two-way binding):
|
||||
/// - The ability read/observe view state
|
||||
/// - The ability to send view events
|
||||
/// - The ability to bind state to a specific portion of the view state safely.
|
||||
/// This class was brought about a little bit by necessity. The most idiomatic way of interacting with SwiftUI is via `@Published`
|
||||
/// properties which which are property wrappers and therefore can't be defined within protocols.
|
||||
/// A similar approach is taken in libraries like [CombineFeedback](https://github.com/sergdort/CombineFeedback).
|
||||
/// It provides a nice layer of consistency and also safety. As we are not passing the `ViewModel` to the view directly, shortcuts/hacks
|
||||
/// can't be made into the `ViewModel`.
|
||||
@available(iOS 14, *)
|
||||
@dynamicMemberLookup
|
||||
class ViewModelContext<ViewState:BindableState, ViewAction>: ObservableObject {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
fileprivate let viewActions: PassthroughSubject<ViewAction, Never>
|
||||
|
||||
// MARK: Public
|
||||
|
||||
/// Get-able/Observable `Published` property for the `ViewState`
|
||||
@Published fileprivate(set) var viewState: ViewState
|
||||
|
||||
/// Set-able/Bindable access to the bindable state.
|
||||
subscript<T>(dynamicMember keyPath: WritableKeyPath<ViewState.BindStateType, T>) -> T {
|
||||
get { viewState.bindings[keyPath: keyPath] }
|
||||
set { viewState.bindings[keyPath: keyPath] = newValue }
|
||||
}
|
||||
|
||||
// MARK: Setup
|
||||
|
||||
init(initialViewState: ViewState) {
|
||||
self.viewActions = PassthroughSubject()
|
||||
self.viewState = initialViewState
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
/// Send a `ViewAction` to the `ViewModel` for processing.
|
||||
/// - Parameter viewAction: The `ViewAction` to send to the `ViewModel`.
|
||||
func send(viewAction: ViewAction) {
|
||||
viewActions.send(viewAction)
|
||||
}
|
||||
}
|
||||
|
||||
/// A common ViewModel implementation for handling of `State`, `StateAction`s and `ViewAction`s
|
||||
///
|
||||
/// Generic type State is constrained to the BindableState protocol in that it may contain (but doesn't have to)
|
||||
/// a specific portion of state that can be safely bound to.
|
||||
/// If we decide to add more features to our state management (like doing state processing off the main thread)
|
||||
/// we can do it in this centralised place.
|
||||
@available(iOS 14, *)
|
||||
class StateStoreViewModel<State: BindableState, StateAction, ViewAction> {
|
||||
|
||||
typealias Context = ViewModelContext<State, ViewAction>
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Public
|
||||
|
||||
/// For storing subscription references.
|
||||
///
|
||||
/// Left as public for `ViewModel` implementations convenience.
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
/// Constrained interface for passing to Views.
|
||||
var context: Context
|
||||
|
||||
/// State can be read within the 'ViewModel' but not modified outside of the reducer.
|
||||
var state: State {
|
||||
context.viewState
|
||||
}
|
||||
|
||||
// MARK: Setup
|
||||
|
||||
init(initialViewState: State) {
|
||||
self.context = Context(initialViewState: initialViewState)
|
||||
self.context.viewActions.sink { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.process(viewAction: action)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
/// Send state actions to modify the state within the reducer.
|
||||
/// - Parameter action: The state action to send to the reducer.
|
||||
func dispatch(action: StateAction) {
|
||||
Self.reducer(state: &context.viewState, action: action)
|
||||
}
|
||||
|
||||
/// Send state actions from a publisher to modify the state within the reducer.
|
||||
/// - Parameter actionPublisher: The publisher that produces actions to be sent to the reducer
|
||||
func dispatch(actionPublisher: AnyPublisher<StateAction, Never>) {
|
||||
actionPublisher.sink { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
Self.reducer(state: &self.context.viewState, action: action)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
/// Override to handle mutations to the `State`
|
||||
///
|
||||
/// A redux style reducer, all modifications to state happen here.
|
||||
/// - Parameters:
|
||||
/// - state: The `inout` state to be modified,
|
||||
/// - action: The action that defines which state modification should take place.
|
||||
class func reducer(state: inout State, action: StateAction) {
|
||||
//Default implementation, -no-op
|
||||
}
|
||||
|
||||
/// Override to handles incoming `ViewAction`s from the `ViewModel`.
|
||||
/// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation.
|
||||
func process(viewAction: ViewAction) {
|
||||
//Default implementation, -no-op
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordin
|
||||
|
||||
init(room: MXRoom, presentedModally: Bool = true) {
|
||||
let roomNotificationService = MXRoomNotificationSettingsService(room: room)
|
||||
let avatarData: AvatarType?
|
||||
let avatarData: AvatarProtocol?
|
||||
let showAvatar = presentedModally
|
||||
if #available(iOS 14.0.0, *) {
|
||||
avatarData = showAvatar ? AvatarInput(
|
||||
@@ -64,7 +64,7 @@ final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordin
|
||||
avatarData: avatarData,
|
||||
displayName: room.summary.displayname,
|
||||
roomEncrypted: room.summary.isEncrypted)
|
||||
let avatarService: AvatarServiceType = AvatarService(mediaManager: room.mxSession.mediaManager)
|
||||
let avatarService: AvatarServiceProtocol = AvatarService(mediaManager: room.mxSession.mediaManager)
|
||||
let view = RoomNotificationSettings(viewModel: swiftUIViewModel, presentedModally: presentedModally)
|
||||
.addDependency(avatarService)
|
||||
let host = VectorHostingController(rootView: view)
|
||||
|
||||
@@ -21,7 +21,7 @@ struct RoomNotificationSettingsViewState: RoomNotificationSettingsViewStateType
|
||||
let roomEncrypted: Bool
|
||||
var saving: Bool
|
||||
var notificationState: RoomNotificationState
|
||||
var avatarData: AvatarType?
|
||||
var avatarData: AvatarProtocol?
|
||||
var displayName: String?
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ protocol RoomNotificationSettingsViewStateType {
|
||||
var roomEncrypted: Bool { get }
|
||||
var notificationOptions: [RoomNotificationState] { get }
|
||||
var notificationState: RoomNotificationState { get }
|
||||
var avatarData: AvatarType? { get }
|
||||
var avatarData: AvatarProtocol? { get }
|
||||
var displayName: String? { get }
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ struct RoomNotificationSettings: View {
|
||||
|
||||
var body: some View {
|
||||
VectorForm {
|
||||
if let avatarData = viewModel.viewState.avatarData as? AvatarInputType {
|
||||
if let avatarData = viewModel.viewState.avatarData as? AvatarInputProtocol {
|
||||
RoomNotificationSettingsHeader(
|
||||
avatarData: avatarData,
|
||||
displayName: viewModel.viewState.displayName
|
||||
|
||||
@@ -20,7 +20,7 @@ import SwiftUI
|
||||
struct RoomNotificationSettingsHeader: View {
|
||||
|
||||
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||
var avatarData: AvatarInputType
|
||||
var avatarData: AvatarInputProtocol
|
||||
var displayName: String?
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -55,7 +55,7 @@ class RoomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType {
|
||||
|
||||
convenience init(
|
||||
roomNotificationService: RoomNotificationSettingsServiceType,
|
||||
avatarData: AvatarType?,
|
||||
avatarData: AvatarProtocol?,
|
||||
displayName: String?,
|
||||
roomEncrypted: Bool
|
||||
) {
|
||||
|
||||
@@ -17,14 +17,12 @@
|
||||
import Foundation
|
||||
import DesignKit
|
||||
|
||||
/**
|
||||
Conformance of MXPushRule to the abstraction `NotificationPushRule` for use in `NotificationSettingsViewModel`.
|
||||
*/
|
||||
// Conformance of MXPushRule to the abstraction `NotificationPushRule` for use in `NotificationSettingsViewModel`.
|
||||
extension MXPushRule: NotificationPushRuleType {
|
||||
|
||||
/*
|
||||
Given a rule, check it match the actions in the static definition.
|
||||
*/
|
||||
/// Given a rule, check it match the actions in the static definition.
|
||||
/// - Parameter standardActions: The standard actions to match against.
|
||||
/// - Returns: Wether `this` rule matches the standard actions.
|
||||
func matches(standardActions: NotificationStandardActions?) -> Bool {
|
||||
guard let standardActions = standardActions else {
|
||||
return false
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
The actions defined on a push rule, used in the static push rule definitions.
|
||||
*/
|
||||
/// The actions defined on a push rule, used in the static push rule definitions.
|
||||
struct NotificationActions {
|
||||
let notify: Bool
|
||||
let highlight: Bool
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Index that determines the state of the push setting.
|
||||
Silent case is un-unsed on iOS but keeping in for consistency of
|
||||
definition across the platforms.
|
||||
*/
|
||||
/// Index that determines the state of the push setting.
|
||||
///
|
||||
/// Silent case is un-used on iOS but keeping in for consistency of
|
||||
/// definition across the platforms.
|
||||
enum NotificationIndex {
|
||||
case off
|
||||
case silent
|
||||
@@ -30,16 +29,14 @@ enum NotificationIndex {
|
||||
extension NotificationIndex: CaseIterable { }
|
||||
|
||||
extension NotificationIndex {
|
||||
/**
|
||||
Used to map the on/off checkmarks to an index used in the static push rule definitions.
|
||||
*/
|
||||
/// Used to map the on/off checkmarks to an index used in the static push rule definitions.
|
||||
/// - Parameter enabled: Enabled/Disabled state.
|
||||
/// - Returns: The associated NotificationIndex
|
||||
static func index(when enabled: Bool) -> NotificationIndex {
|
||||
return enabled ? .noisy : .off
|
||||
}
|
||||
|
||||
/**
|
||||
Used to map from the checked state back to the index.
|
||||
*/
|
||||
/// Used to map from the checked state back to the index.
|
||||
var enabled: Bool {
|
||||
return self != .off
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ import Foundation
|
||||
|
||||
|
||||
extension NotificationPushRuleId {
|
||||
/**
|
||||
A static definition of the push rule actions.
|
||||
It is defined similarly across Web and Android.
|
||||
*/
|
||||
/// A static definition of the push rule actions.
|
||||
///
|
||||
/// It is defined similarly across Web and Android.
|
||||
/// - Parameter index: The notification index for which to get the actions for.
|
||||
/// - Returns: The associated `NotificationStandardActions`.
|
||||
func standardActions(for index: NotificationIndex) -> NotificationStandardActions? {
|
||||
switch self {
|
||||
case .containDisplayName:
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
The push rule ids used in notification settings and the static rule definitions.
|
||||
*/
|
||||
/// The push rule ids used in notification settings and the static rule definitions.
|
||||
enum NotificationPushRuleId: String {
|
||||
case suppressBots = ".m.rule.suppress_notices"
|
||||
case inviteMe = ".m.rule.invite_for_me"
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
The notification settings screen definitions, used when calling the coordinator.
|
||||
*/
|
||||
/// The notification settings screen definitions, used when calling the coordinator.
|
||||
@objc enum NotificationSettingsScreen: Int {
|
||||
case defaultNotifications
|
||||
case mentionsAndKeywords
|
||||
@@ -32,9 +30,7 @@ extension NotificationSettingsScreen: Identifiable {
|
||||
}
|
||||
|
||||
extension NotificationSettingsScreen {
|
||||
/**
|
||||
Defines which rules are handled by each of the screens.
|
||||
*/
|
||||
/// Defines which rules are handled by each of the screens.
|
||||
var pushRules: [NotificationPushRuleId] {
|
||||
switch self {
|
||||
case .defaultNotifications:
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A static definition of the different actions that can be defined on push rules.
|
||||
It is defined similarly across Web and Android.
|
||||
*/
|
||||
/// A static definition of the different actions that can be defined on push rules.
|
||||
///
|
||||
/// It is defined similarly across Web and Android.
|
||||
enum NotificationStandardActions {
|
||||
case notify
|
||||
case notifyDefaultSound
|
||||
|
||||
@@ -17,41 +17,29 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/**
|
||||
A service for changing notification settings and keywords
|
||||
*/
|
||||
/// A service for changing notification settings and keywords
|
||||
@available(iOS 14.0, *)
|
||||
protocol NotificationSettingsServiceType {
|
||||
/**
|
||||
Publisher of all push rules.
|
||||
*/
|
||||
/// Publisher of all push rules.
|
||||
var rulesPublisher: AnyPublisher<[NotificationPushRuleType], Never> { get }
|
||||
/**
|
||||
Publisher of content rules.
|
||||
*/
|
||||
|
||||
/// Publisher of content rules.
|
||||
var contentRulesPublisher: AnyPublisher<[NotificationPushRuleType], Never> { get }
|
||||
/**
|
||||
Adds a keyword.
|
||||
|
||||
- Parameters:
|
||||
- keyword: The keyword to add.
|
||||
- enabled: Whether the keyword should be added in the enabled or disabled state.
|
||||
*/
|
||||
|
||||
/// Adds a keyword.
|
||||
/// - Parameters:
|
||||
/// - keyword: The keyword to add.
|
||||
/// - enabled: Whether the keyword should be added in the enabled or disabled state.
|
||||
func add(keyword: String, enabled: Bool)
|
||||
/**
|
||||
Removes a keyword.
|
||||
|
||||
- Parameters:
|
||||
- keyword: The keyword to remove.
|
||||
*/
|
||||
|
||||
/// Removes a keyword.
|
||||
/// - Parameter keyword: The keyword to remove.
|
||||
func remove(keyword: String)
|
||||
/**
|
||||
Updates the push rule actions.
|
||||
|
||||
- Parameters:
|
||||
- ruleId: The id of the rule.
|
||||
- enabled: Whether the rule should be enabled or disabled.
|
||||
- actions: The actions to update with.
|
||||
*/
|
||||
|
||||
/// Updates the push rule actions.
|
||||
/// - Parameters:
|
||||
/// - ruleId: The id of the rule.
|
||||
/// - enabled: Whether the rule should be enabled or disabled.
|
||||
/// - actions: The actions to update with.
|
||||
func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
A bordered style of text input as defined in:
|
||||
https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=2039%3A26415
|
||||
*/
|
||||
@available(iOS 14.0, *)
|
||||
/// A bordered style of text input
|
||||
///
|
||||
/// As defined in:
|
||||
/// https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=2039%3A26415
|
||||
struct BorderedInputFieldStyle: TextFieldStyle {
|
||||
|
||||
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
A single rounded rect chip to be rendered within `Chips` collection
|
||||
*/
|
||||
|
||||
/// A single rounded rect chip to be rendered within `Chips` collection
|
||||
@available(iOS 14.0, *)
|
||||
struct Chip: View {
|
||||
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
Renders multiple chips in a flow layout.
|
||||
*/
|
||||
/// Renders multiple chips in a flow layout.
|
||||
@available(iOS 14.0, *)
|
||||
struct Chips: View {
|
||||
|
||||
|
||||
@@ -16,11 +16,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
/**
|
||||
Renders an input field and a collection of chips
|
||||
with callbacks for addition and deletion.
|
||||
*/
|
||||
/// Renders an input field and a collection of chips.
|
||||
@available(iOS 14.0, *)
|
||||
struct ChipsInput: View {
|
||||
|
||||
@@ -29,7 +25,6 @@ struct ChipsInput: View {
|
||||
|
||||
@State private var chipText: String = ""
|
||||
|
||||
|
||||
let titles: [String]
|
||||
let didAddChip: (String) -> Void
|
||||
let didDeleteChip: (String) -> Void
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
An input field for forms.
|
||||
*/
|
||||
/// An input field style for forms.
|
||||
@available(iOS 14.0, *)
|
||||
struct FormInputFieldStyle: TextFieldStyle {
|
||||
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
Renders the push rule settings that can be enabled/disable.
|
||||
Also renders an optional bottom section
|
||||
(used in the case of keywords, for the keyword chips and input).
|
||||
*/
|
||||
/// Renders the push rule settings that can be enabled/disable.
|
||||
///
|
||||
/// Also renders an optional bottom section.
|
||||
/// Used in the case of keywords, for the keyword chips and input.
|
||||
@available(iOS 14.0, *)
|
||||
struct NotificationSettings<BottomSection: View>: View {
|
||||
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
Renders the keywords input, driven by 'NotificationSettingsViewModel'.
|
||||
*/
|
||||
/// Renders the keywords input, driven by 'NotificationSettingsViewModel'.
|
||||
@available(iOS 14.0, *)
|
||||
struct NotificationSettingsKeywords: View {
|
||||
@ObservedObject var viewModel: NotificationSettingsViewModel
|
||||
|
||||
@@ -168,12 +168,13 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob
|
||||
self.viewState.selectionState[.keywords] = anyEnabled
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Given a push rule check which index/checked state it matches.
|
||||
Matcing is done by comparing the rule against the static definitions for that rule.
|
||||
The same logic is used on android.
|
||||
*/
|
||||
|
||||
/// Given a push rule check which index/checked state it matches.
|
||||
///
|
||||
/// Matching is done by comparing the rule against the static definitions for that rule.
|
||||
/// The same logic is used on android.
|
||||
/// - Parameter rule: The push rule type to check.
|
||||
/// - Returns: Wether it should be displayed as checked or not checked.
|
||||
private func isChecked(rule: NotificationPushRuleType) -> Bool {
|
||||
guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false }
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2021 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 SwiftUI
|
||||
|
||||
final class TemplateUserProfileCoordinator: Coordinator {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: TemplateUserProfileCoordinatorParameters
|
||||
private let templateUserProfileHostingController: UIViewController
|
||||
private var templateUserProfileViewModel: TemplateUserProfileViewModelProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: (() -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
init(parameters: TemplateUserProfileCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileService(session: parameters.session))
|
||||
let view = TemplateUserProfile(viewModel: viewModel.context)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
|
||||
templateUserProfileViewModel = viewModel
|
||||
templateUserProfileHostingController = VectorHostingController(rootView: view)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func start() {
|
||||
MXLog.debug("[TemplateUserProfileCoordinator] did start.")
|
||||
templateUserProfileViewModel.completion = { [weak self] result in
|
||||
MXLog.debug("[TemplateUserProfileCoordinator] TemplateUserProfileViewModel did complete with result: \(result).")
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .cancel, .done:
|
||||
self.completion?()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.templateUserProfileHostingController
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2021 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 TemplateUserProfileCoordinatorParameters {
|
||||
let session: MXSession
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Copyright 2021 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 TemplateUserProfilePresence {
|
||||
case online
|
||||
case idle
|
||||
case offline
|
||||
}
|
||||
|
||||
extension TemplateUserProfilePresence {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .online:
|
||||
return VectorL10n.roomParticipantsOnline
|
||||
case .idle:
|
||||
return VectorL10n.roomParticipantsIdle
|
||||
case .offline:
|
||||
return VectorL10n.roomParticipantsOffline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TemplateUserProfilePresence: CaseIterable { }
|
||||
|
||||
extension TemplateUserProfilePresence: Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2021 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 TemplateUserProfileStateAction {
|
||||
case viewAction(TemplateUserProfileViewAction)
|
||||
case updatePresence(TemplateUserProfilePresence)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Copyright 2021 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 TemplateUserProfileViewAction {
|
||||
case incrementCount
|
||||
case decrementCount
|
||||
case cancel
|
||||
case done
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2021 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 TemplateUserProfileViewModelResult {
|
||||
case cancel
|
||||
case done
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Copyright 2021 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 TemplateUserProfileViewState: BindableState {
|
||||
let avatar: AvatarInputProtocol?
|
||||
let displayName: String?
|
||||
var presence: TemplateUserProfilePresence
|
||||
var count: Int
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// Copyright 2021 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 Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateUserProfileService: TemplateUserProfileServiceProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let session: MXSession
|
||||
private var listenerReference: Any?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var userId: String {
|
||||
session.myUser.userId
|
||||
}
|
||||
|
||||
var displayName: String? {
|
||||
session.myUser.displayname
|
||||
}
|
||||
|
||||
var avatarUrl: String? {
|
||||
session.myUser.avatarUrl
|
||||
}
|
||||
|
||||
private(set) var presenceSubject: CurrentValueSubject<TemplateUserProfilePresence, Never>
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(session: MXSession) {
|
||||
self.session = session
|
||||
self.presenceSubject = CurrentValueSubject(TemplateUserProfilePresence(mxPresence: session.myUser.presence))
|
||||
self.listenerReference = setupPresenceListener()
|
||||
}
|
||||
|
||||
deinit {
|
||||
guard let reference = listenerReference else { return }
|
||||
session.myUser.removeListener(reference)
|
||||
}
|
||||
|
||||
func setupPresenceListener() -> Any? {
|
||||
let reference = session.myUser.listen { [weak self] event in
|
||||
guard let self = self,
|
||||
let event = event,
|
||||
case .presence = MXEventType(identifier: event.eventId)
|
||||
else { return }
|
||||
self.presenceSubject.send(TemplateUserProfilePresence(mxPresence: self.session.myUser.presence))
|
||||
}
|
||||
if reference == nil {
|
||||
UILog.error("[TemplateUserProfileService] Did not recieve a lisenter reference.")
|
||||
}
|
||||
return reference
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension TemplateUserProfilePresence {
|
||||
|
||||
init(mxPresence: MXPresence) {
|
||||
switch mxPresence {
|
||||
case MXPresenceOnline:
|
||||
self = .online
|
||||
case MXPresenceUnavailable:
|
||||
self = .idle
|
||||
case MXPresenceOffline, MXPresenceUnknown:
|
||||
self = .offline
|
||||
default:
|
||||
self = .offline
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// Copyright 2021 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 SwiftUI
|
||||
|
||||
|
||||
/// Using an enum for the screen allows you define the different state cases with
|
||||
/// the relevant associated data for each case.
|
||||
@available(iOS 14.0, *)
|
||||
enum MockTemplateUserProfileScreenState: MockScreenState, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
case presence(TemplateUserProfilePresence)
|
||||
case longDisplayName(String)
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
TemplateUserProfile.self
|
||||
}
|
||||
|
||||
/// A list of screen state definitions
|
||||
static var allCases: [MockTemplateUserProfileScreenState] {
|
||||
// Each of the presence statuses
|
||||
TemplateUserProfilePresence.allCases.map(MockTemplateUserProfileScreenState.presence)
|
||||
// A long display name
|
||||
+ [.longDisplayName("Somebody with a super long name we would like to test")]
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: AnyView {
|
||||
let service: MockTemplateUserProfileService
|
||||
switch self {
|
||||
case .presence(let presence):
|
||||
service = MockTemplateUserProfileService(presence: presence)
|
||||
case .longDisplayName(let displayName):
|
||||
service = MockTemplateUserProfileService(displayName: displayName)
|
||||
}
|
||||
let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: service)
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return AnyView(TemplateUserProfile(viewModel: viewModel.context)
|
||||
.addDependency(MockAvatarService.example))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Copyright 2021 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 Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class MockTemplateUserProfileService: TemplateUserProfileServiceProtocol {
|
||||
var presenceSubject: CurrentValueSubject<TemplateUserProfilePresence, Never>
|
||||
|
||||
let userId: String
|
||||
let displayName: String?
|
||||
let avatarUrl: String?
|
||||
init(
|
||||
userId: String = "@alice:matrix.org",
|
||||
displayName: String? = "Alice",
|
||||
avatarUrl: String? = "mxc://matrix.org/VyNYAgahaiAzUoOeZETtQ",
|
||||
presence: TemplateUserProfilePresence = .offline
|
||||
) {
|
||||
self.userId = userId
|
||||
self.displayName = displayName
|
||||
self.avatarUrl = avatarUrl
|
||||
self.presenceSubject = CurrentValueSubject<TemplateUserProfilePresence, Never>(presence)
|
||||
}
|
||||
|
||||
func simulateUpdate(presence: TemplateUserProfilePresence) {
|
||||
self.presenceSubject.value = presence
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright 2021 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 Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol TemplateUserProfileServiceProtocol: Avatarable {
|
||||
var userId: String { get }
|
||||
var displayName: String? { get }
|
||||
var avatarUrl: String? { get }
|
||||
var presenceSubject: CurrentValueSubject<TemplateUserProfilePresence, Never> { get }
|
||||
}
|
||||
|
||||
// MARK: Avatarable
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension TemplateUserProfileServiceProtocol {
|
||||
var mxContentUri: String? {
|
||||
avatarUrl
|
||||
}
|
||||
var matrixItemId: String {
|
||||
userId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright 2021 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 XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateUserProfileUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockTemplateUserProfileScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return TemplateUserProfileUITests(selector: #selector(verifyTemplateUserProfileScreen))
|
||||
}
|
||||
|
||||
func verifyTemplateUserProfileScreen() throws {
|
||||
guard let screenState = screenState as? MockTemplateUserProfileScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .presence(let presence):
|
||||
verifyTemplateUserProfilePresence(presence: presence)
|
||||
case .longDisplayName(let name):
|
||||
verifyTemplateUserProfileLongName(name: name)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyTemplateUserProfilePresence(presence: TemplateUserProfilePresence) {
|
||||
let presenceText = app.staticTexts["presenceText"]
|
||||
XCTAssert(presenceText.exists)
|
||||
XCTAssertEqual(presenceText.label, presence.title)
|
||||
}
|
||||
|
||||
func verifyTemplateUserProfileLongName(name: String) {
|
||||
let displayNameText = app.staticTexts["displayNameText"]
|
||||
XCTAssert(displayNameText.exists)
|
||||
XCTAssertEqual(displayNameText.label, name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// Copyright 2021 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 XCTest
|
||||
import Combine
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateUserProfileViewModelTests: XCTestCase {
|
||||
private enum Constants {
|
||||
static let presenceInitialValue: TemplateUserProfilePresence = .offline
|
||||
static let displayName = "Alice"
|
||||
}
|
||||
var service: MockTemplateUserProfileService!
|
||||
var viewModel: TemplateUserProfileViewModelProtocol!
|
||||
var context: TemplateUserProfileViewModelType.Context!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
override func setUpWithError() throws {
|
||||
service = MockTemplateUserProfileService(displayName: Constants.displayName, presence: Constants.presenceInitialValue)
|
||||
viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: service)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertEqual(context.viewState.displayName, Constants.displayName)
|
||||
XCTAssertEqual(context.viewState.presence, Constants.presenceInitialValue)
|
||||
}
|
||||
|
||||
func testFirstPresenceReceived() throws {
|
||||
let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(1).first()
|
||||
XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue])
|
||||
}
|
||||
|
||||
func testPresenceUpdatesReceived() throws {
|
||||
let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(3).first()
|
||||
let awaitDeferred = xcAwaitDeferred(presencePublisher)
|
||||
let newPresenceValue1: TemplateUserProfilePresence = .online
|
||||
let newPresenceValue2: TemplateUserProfilePresence = .idle
|
||||
service.simulateUpdate(presence: newPresenceValue1)
|
||||
service.simulateUpdate(presence: newPresenceValue2)
|
||||
XCTAssertEqual(try awaitDeferred(), [Constants.presenceInitialValue, newPresenceValue1, newPresenceValue2])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateUserProfile: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: TemplateUserProfileViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
EmptyView()
|
||||
VStack {
|
||||
TemplateUserProfileHeader(
|
||||
avatar: viewModel.viewState.avatar,
|
||||
displayName: viewModel.viewState.displayName,
|
||||
presence: viewModel.viewState.presence
|
||||
)
|
||||
Divider()
|
||||
HStack{
|
||||
Text("Counter: \(viewModel.viewState.count)")
|
||||
.font(theme.fonts.title2)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
Button("-") {
|
||||
viewModel.send(viewAction: .decrementCount)
|
||||
}
|
||||
Button("+") {
|
||||
viewModel.send(viewAction: .incrementCount)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.background(theme.colors.background)
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationTitle(viewModel.viewState.displayName ?? "")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(VectorL10n.done) {
|
||||
viewModel.send(viewAction: .done)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(VectorL10n.cancel) {
|
||||
viewModel.send(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateUserProfile_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MockTemplateUserProfileScreenState.screenGroup()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateUserProfileHeader: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
let avatar: AvatarInputProtocol?
|
||||
let displayName: String?
|
||||
let presence: TemplateUserProfilePresence
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let avatar = avatar {
|
||||
AvatarImage(avatarData: avatar, size: .xxLarge)
|
||||
.padding(.vertical)
|
||||
}
|
||||
VStack(spacing: 8){
|
||||
Text(displayName ?? "")
|
||||
.font(theme.fonts.title3)
|
||||
.accessibility(identifier: "displayNameText")
|
||||
.padding(.horizontal)
|
||||
.lineLimit(1)
|
||||
TemplateUserProfilePresenceView(presence: presence)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateUserProfileHeader_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TemplateUserProfileHeader(avatar: MockAvatarInput.example, displayName: "Alice", presence: .online)
|
||||
.addDependency(MockAvatarService.example)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateUserProfilePresenceView: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Public
|
||||
let presence: TemplateUserProfilePresence
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: "circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 8, height: 8)
|
||||
Text(presence.title)
|
||||
.font(.subheadline)
|
||||
.accessibilityIdentifier("presenceText")
|
||||
}
|
||||
.foregroundColor(foregroundColor)
|
||||
.padding(0)
|
||||
}
|
||||
|
||||
// MARK: View Components
|
||||
|
||||
private var foregroundColor: Color {
|
||||
switch presence {
|
||||
case .online:
|
||||
return .green
|
||||
case .idle:
|
||||
return .orange
|
||||
case .offline:
|
||||
return .gray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateUserProfilePresenceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(alignment:.leading){
|
||||
Text("Presence")
|
||||
ForEach(TemplateUserProfilePresence.allCases) { presence in
|
||||
TemplateUserProfilePresenceView(presence: presence)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
import Combine
|
||||
|
||||
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias TemplateUserProfileViewModelType = StateStoreViewModel<TemplateUserProfileViewState,
|
||||
TemplateUserProfileStateAction,
|
||||
TemplateUserProfileViewAction>
|
||||
@available(iOS 14, *)
|
||||
class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUserProfileViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let templateUserProfileService: TemplateUserProfileServiceProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var completion: ((TemplateUserProfileViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
static func makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewModelProtocol {
|
||||
return TemplateUserProfileViewModel(templateUserProfileService: templateUserProfileService)
|
||||
}
|
||||
|
||||
private init(templateUserProfileService: TemplateUserProfileServiceProtocol) {
|
||||
self.templateUserProfileService = templateUserProfileService
|
||||
super.init(initialViewState: Self.defaultState(templateUserProfileService: templateUserProfileService))
|
||||
setupPresenceObserving()
|
||||
}
|
||||
|
||||
private static func defaultState(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewState {
|
||||
return TemplateUserProfileViewState(
|
||||
avatar: templateUserProfileService.avatarData,
|
||||
displayName: templateUserProfileService.displayName,
|
||||
presence: templateUserProfileService.presenceSubject.value,
|
||||
count: 0
|
||||
)
|
||||
}
|
||||
|
||||
private func setupPresenceObserving() {
|
||||
let presenceUpdatePublisher = templateUserProfileService.presenceSubject
|
||||
.map(TemplateUserProfileStateAction.updatePresence)
|
||||
.eraseToAnyPublisher()
|
||||
dispatch(actionPublisher: presenceUpdatePublisher)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: TemplateUserProfileViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
cancel()
|
||||
case .done:
|
||||
done()
|
||||
case .incrementCount, .decrementCount:
|
||||
dispatch(action: .viewAction(viewAction))
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout TemplateUserProfileViewState, action: TemplateUserProfileStateAction) {
|
||||
switch action {
|
||||
case .updatePresence(let presence):
|
||||
state.presence = presence
|
||||
case .viewAction(let viewAction):
|
||||
switch viewAction {
|
||||
case .incrementCount:
|
||||
state.count += 1
|
||||
case .decrementCount:
|
||||
state.count -= 1
|
||||
case .cancel, .done:
|
||||
break
|
||||
}
|
||||
}
|
||||
UILog.debug("[TemplateUserProfileViewModel] reducer with action \(action) produced state: \(state)")
|
||||
}
|
||||
|
||||
private func done() {
|
||||
completion?(.done)
|
||||
}
|
||||
|
||||
private func cancel() {
|
||||
completion?(.cancel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Copyright 2021 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 TemplateUserProfileViewModelProtocol {
|
||||
|
||||
var completion: ((TemplateUserProfileViewModelResult) -> Void)? { get set }
|
||||
@available(iOS 14, *)
|
||||
static func makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewModelProtocol
|
||||
@available(iOS 14, *)
|
||||
var context: TemplateUserProfileViewModelType.Context { get }
|
||||
}
|
||||
@@ -15,16 +15,16 @@
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
Just needed so the application target has an entry point for the moment.
|
||||
Could use to render the different screens.
|
||||
*/
|
||||
@available(iOS 14.0, *)
|
||||
@main
|
||||
struct testApp: App {
|
||||
/// RiotSwiftUI screens rendered for UI Tests.
|
||||
struct RiotSwiftUIApp: App {
|
||||
init() {
|
||||
UILog.configure(logger: PrintLogger.self)
|
||||
}
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
Text("app")
|
||||
ScreenList(screens: MockAppScreens.appScreens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ targets:
|
||||
# Don't include service implementations and coordinator/bridges in target.
|
||||
- "**/MatrixSDK/**"
|
||||
- "**/Coordinator/**"
|
||||
- "**/Test/**"
|
||||
- path: ../Riot/Generated/Strings.swift
|
||||
- path: ../Riot/Generated/Images.swift
|
||||
- path: ../Riot/Managers/Theme/ThemeIdentifier.swift
|
||||
|
||||
57
RiotSwiftUI/targetUITests.yml
Normal file
57
RiotSwiftUI/targetUITests.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
name: RiotSwiftUITests
|
||||
|
||||
schemes:
|
||||
RiotSwiftUITests:
|
||||
analyze:
|
||||
config: Debug
|
||||
archive:
|
||||
config: Release
|
||||
build:
|
||||
targets:
|
||||
RiotSwiftUITests:
|
||||
- running
|
||||
- testing
|
||||
- profiling
|
||||
- analyzing
|
||||
- archiving
|
||||
profile:
|
||||
config: Release
|
||||
run:
|
||||
config: Debug
|
||||
disableMainThreadChecker: true
|
||||
test:
|
||||
config: Debug
|
||||
disableMainThreadChecker: true
|
||||
targets:
|
||||
- RiotSwiftUITests
|
||||
|
||||
targets:
|
||||
RiotSwiftUITests:
|
||||
type: bundle.ui-testing
|
||||
platform: iOS
|
||||
|
||||
dependencies:
|
||||
- target: RiotSwiftUI
|
||||
|
||||
settings:
|
||||
base:
|
||||
TEST_TARGET_NAME: RiotSwiftUI
|
||||
PRODUCT_BUNDLE_IDENTIFIER: org.matrix.RiotSwiftUITests$(rfc1034identifier)
|
||||
sources:
|
||||
# Source included/excluded here here are similar to RiotSwiftUI as we
|
||||
# need access to ScreenStates
|
||||
- path: ../RiotSwiftUI/Modules
|
||||
excludes:
|
||||
- "**/MatrixSDK/**"
|
||||
- "**/Coordinator/**"
|
||||
- "**/Test/Unit/**"
|
||||
- path: ../Riot/Generated/Strings.swift
|
||||
- path: ../Riot/Generated/Images.swift
|
||||
- path: ../Riot/Managers/Theme/ThemeIdentifier.swift
|
||||
- path: ../Riot/Managers/Locale/LocaleProviderType.swift
|
||||
- path: ../Riot/Assets/en.lproj/Vector.strings
|
||||
buildPhase: resources
|
||||
- path: ../Riot/Assets/Images.xcassets
|
||||
buildPhase: resources
|
||||
- path: ../Riot/Assets/SharedImages.xcassets
|
||||
buildPhase: resources
|
||||
57
RiotSwiftUI/targetUnitTests.yml
Normal file
57
RiotSwiftUI/targetUnitTests.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
name: RiotSwiftUnitTests
|
||||
|
||||
schemes:
|
||||
RiotSwiftUnitTests:
|
||||
analyze:
|
||||
config: Debug
|
||||
archive:
|
||||
config: Release
|
||||
build:
|
||||
targets:
|
||||
RiotSwiftUnitTests:
|
||||
- running
|
||||
- testing
|
||||
- profiling
|
||||
- analyzing
|
||||
- archiving
|
||||
profile:
|
||||
config: Release
|
||||
run:
|
||||
config: Debug
|
||||
disableMainThreadChecker: true
|
||||
test:
|
||||
config: Debug
|
||||
disableMainThreadChecker: true
|
||||
targets:
|
||||
- RiotSwiftUnitTests
|
||||
|
||||
targets:
|
||||
RiotSwiftUnitTests:
|
||||
type: bundle.unit-test
|
||||
platform: iOS
|
||||
|
||||
dependencies:
|
||||
- target: RiotSwiftUI
|
||||
|
||||
configFiles:
|
||||
Debug: Debug.xcconfig
|
||||
Release: Release.xcconfig
|
||||
|
||||
settings:
|
||||
base:
|
||||
FRAMEWORK_SEARCH_PATHS: $(SDKROOT)/Developer/Library/Frameworks $(inherited)
|
||||
INFOPLIST_FILE: RiotSwiftUI/Info.plist
|
||||
LD_RUNPATH_SEARCH_PATHS: $(inherited) @executable_path/Frameworks @loader_path/Frameworks
|
||||
PRODUCT_BUNDLE_IDENTIFIER: org.matrix.$(PRODUCT_NAME:rfc1034identifier)
|
||||
PRODUCT_NAME: RiotSwiftUnitTests
|
||||
configs:
|
||||
Debug:
|
||||
Release:
|
||||
PROVISIONING_PROFILE: $(RIOT_PROVISIONING_PROFILE)
|
||||
PROVISIONING_PROFILE_SPECIFIER: $(RIOT_PROVISIONING_PROFILE_SPECIFIER)
|
||||
sources:
|
||||
- path: ../RiotSwiftUI/Modules
|
||||
includes:
|
||||
- "**/Test"
|
||||
excludes:
|
||||
- "**/Test/UI/**"
|
||||
@@ -60,7 +60,7 @@ class RoomNotificationSettingsViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func setupViewModel(roomEncrypted: Bool, showAvatar: Bool) {
|
||||
let avatarData: AvatarType? = showAvatar ? Constants.avatarData : nil
|
||||
let avatarData: AvatarProtocol? = showAvatar ? Constants.avatarData : nil
|
||||
let viewModel = RoomNotificationSettingsViewModel(roomNotificationService: service, avatarData: avatarData, displayName: Constants.roomDisplayName, roomEncrypted: roomEncrypted)
|
||||
viewModel.viewDelegate = view
|
||||
viewModel.coordinatorDelegate = coordinator
|
||||
|
||||
34
Tools/Templates/createSwiftUISingleScreen.sh
Executable file
34
Tools/Templates/createSwiftUISingleScreen.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! $# -eq 2 ]; then
|
||||
echo "Usage: ./createSwiftUISingleScreen.sh Folder MyScreenName"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MODULE_DIR="../../RiotSwiftUI/Modules"
|
||||
OUTPUT_DIR=$MODULE_DIR/$1
|
||||
SCREEN_NAME=$2
|
||||
SCREEN_VAR_NAME=`echo $SCREEN_NAME | awk '{ print tolower(substr($0, 1, 1)) substr($0, 2) }'`
|
||||
TEMPLATE_DIR=$MODULE_DIR/Template/SimpleUserProfileExample/
|
||||
if [ -e $OUTPUT_DIR ]; then
|
||||
echo "Error: Folder ${OUTPUT_DIR} already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Create folder ${OUTPUT_DIR}"
|
||||
|
||||
mkdir -p $OUTPUT_DIR
|
||||
cp -R $TEMPLATE_DIR $OUTPUT_DIR/
|
||||
|
||||
cd $OUTPUT_DIR
|
||||
for file in $(find * -type f -print)
|
||||
do
|
||||
echo "Building ${file/TemplateUserProfile/$SCREEN_NAME}..."
|
||||
perl -p -i -e "s/TemplateUserProfile/"$SCREEN_NAME"/g" $file
|
||||
perl -p -i -e "s/templateUserProfile/"$SCREEN_VAR_NAME"/g" $file
|
||||
|
||||
echo "// $ createScreen.sh $@" | cat - ${file} > /tmp/$$ && mv /tmp/$$ ${file}
|
||||
echo '// File created from SimpleUserProfileExample' | cat - ${file} > /tmp/$$ && mv /tmp/$$ ${file}
|
||||
|
||||
mv ${file} ${file/TemplateUserProfile/$SCREEN_NAME}
|
||||
done
|
||||
1
changelog.d/47773.change
Normal file
1
changelog.d/47773.change
Normal file
@@ -0,0 +1 @@
|
||||
Voice Messages: Pause playback when changing rooms while retaining the playback position when re-entering.
|
||||
@@ -33,3 +33,5 @@ include:
|
||||
- path: RiotNSE/target.yml
|
||||
- path: DesignKit/target.yml
|
||||
- path: RiotSwiftUI/target.yml
|
||||
- path: RiotSwiftUI/targetUnitTests.yml
|
||||
- path: RiotSwiftUI/targetUITests.yml
|
||||
|
||||
Reference in New Issue
Block a user