mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-03 06:36:58 +02:00
Merge commit 'aaadcc73674cc8886e363693a7d7c08ac9b4f516' into feature/4260_merge_foss_1_10_2
# Conflicts: # Config/AppVersion.xcconfig # Podfile # Podfile.lock # Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved # Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift # Riot/Modules/Application/LegacyAppDelegate.m # Riot/Modules/Authentication/AuthenticationCoordinator.swift # Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift # Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift # Riot/Modules/Home/AllChats/AllChatsViewController.swift # Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift # Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift # Riot/Modules/Room/Settings/RoomSettingsViewController.m # fastlane/Fastfile
This commit is contained in:
@@ -32,10 +32,6 @@ class AvatarService: AvatarServiceProtocol {
|
||||
|
||||
private let mediaManager: MXMediaManager
|
||||
|
||||
static func instantiate(mediaManager: MXMediaManager) -> AvatarServiceProtocol {
|
||||
AvatarService(mediaManager: mediaManager)
|
||||
}
|
||||
|
||||
init(mediaManager: MXMediaManager) {
|
||||
self.mediaManager = mediaManager
|
||||
}
|
||||
|
||||
@@ -19,17 +19,18 @@ import SwiftUI
|
||||
|
||||
struct AvatarImage: View {
|
||||
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||
@Environment(\.dependencies) var dependencies: DependencyContainer
|
||||
@StateObject var viewModel = AvatarViewModel()
|
||||
@EnvironmentObject var viewModel: AvatarViewModel
|
||||
|
||||
var mxContentUri: String?
|
||||
var matrixItemId: String
|
||||
var displayName: String?
|
||||
var size: AvatarSize
|
||||
|
||||
@State private var avatar: AvatarViewState = .empty
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch viewModel.viewState {
|
||||
switch avatar {
|
||||
case .empty:
|
||||
ProgressView()
|
||||
case .placeholder(let firstCharacter, let colorIndex):
|
||||
@@ -43,14 +44,16 @@ struct AvatarImage: View {
|
||||
.frame(maxWidth: CGFloat(size.rawValue), maxHeight: CGFloat(size.rawValue))
|
||||
.clipShape(Circle())
|
||||
.onAppear {
|
||||
viewModel.inject(dependencies: dependencies)
|
||||
viewModel.loadAvatar(
|
||||
mxContentUri: mxContentUri,
|
||||
matrixItemId: matrixItemId,
|
||||
displayName: displayName,
|
||||
colorCount: theme.colors.namesAndAvatars.count,
|
||||
avatarSize: size
|
||||
)
|
||||
avatar = viewModel.placeholderAvatar(matrixItemId: matrixItemId,
|
||||
displayName: displayName,
|
||||
colorCount: theme.colors.namesAndAvatars.count)
|
||||
viewModel.loadAvatar(mxContentUri: mxContentUri,
|
||||
matrixItemId: matrixItemId,
|
||||
displayName: displayName,
|
||||
colorCount: theme.colors.namesAndAvatars.count,
|
||||
avatarSize: size ) { newState in
|
||||
avatar = newState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +98,7 @@ struct AvatarImage_Previews: PreviewProvider {
|
||||
AvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .xLarge)
|
||||
}
|
||||
}
|
||||
.addDependency(MockAvatarService.example)
|
||||
.environmentObject(AvatarViewModel.withMockedServices())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,17 +19,18 @@ import SwiftUI
|
||||
|
||||
struct SpaceAvatarImage: View {
|
||||
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||
@Environment(\.dependencies) var dependencies: DependencyContainer
|
||||
@StateObject var viewModel = AvatarViewModel()
|
||||
@EnvironmentObject var viewModel: AvatarViewModel
|
||||
|
||||
var mxContentUri: String?
|
||||
var matrixItemId: String
|
||||
var displayName: String?
|
||||
var size: AvatarSize
|
||||
|
||||
@State private var avatar: AvatarViewState = .empty
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch viewModel.viewState {
|
||||
switch avatar {
|
||||
case .empty:
|
||||
ProgressView()
|
||||
case .placeholder(let firstCharacter, let colorIndex):
|
||||
@@ -49,24 +50,27 @@ struct SpaceAvatarImage: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
.onChange(of: displayName, perform: { value in
|
||||
viewModel.loadAvatar(
|
||||
mxContentUri: mxContentUri,
|
||||
matrixItemId: matrixItemId,
|
||||
displayName: value,
|
||||
colorCount: theme.colors.namesAndAvatars.count,
|
||||
avatarSize: size
|
||||
)
|
||||
})
|
||||
.onChange(of: displayName) { value in
|
||||
guard case .placeholder = avatar else { return }
|
||||
viewModel.loadAvatar(mxContentUri: mxContentUri,
|
||||
matrixItemId: matrixItemId,
|
||||
displayName: value,
|
||||
colorCount: theme.colors.namesAndAvatars.count,
|
||||
avatarSize: size) { newState in
|
||||
avatar = newState
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.inject(dependencies: dependencies)
|
||||
viewModel.loadAvatar(
|
||||
mxContentUri: mxContentUri,
|
||||
matrixItemId: matrixItemId,
|
||||
displayName: displayName,
|
||||
colorCount: theme.colors.namesAndAvatars.count,
|
||||
avatarSize: size
|
||||
)
|
||||
avatar = viewModel.placeholderAvatar(matrixItemId: matrixItemId,
|
||||
displayName: displayName,
|
||||
colorCount: theme.colors.namesAndAvatars.count)
|
||||
viewModel.loadAvatar(mxContentUri: mxContentUri,
|
||||
matrixItemId: matrixItemId,
|
||||
displayName: displayName,
|
||||
colorCount: theme.colors.namesAndAvatars.count,
|
||||
avatarSize: size) { newState in
|
||||
avatar = newState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +103,7 @@ struct LiveAvatarImage_Previews: PreviewProvider {
|
||||
SpaceAvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .xLarge)
|
||||
}
|
||||
}
|
||||
.addDependency(MockAvatarService.example)
|
||||
.environmentObject(AvatarViewModel.withMockedServices())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,25 @@ import DesignKit
|
||||
import Foundation
|
||||
|
||||
/// Simple ViewModel that supports loading an avatar image
|
||||
class AvatarViewModel: InjectableObject, ObservableObject {
|
||||
@Inject var avatarService: AvatarServiceProtocol
|
||||
final class AvatarViewModel: ObservableObject {
|
||||
private let avatarService: AvatarServiceProtocol
|
||||
|
||||
@Published private(set) var viewState = AvatarViewState.empty
|
||||
init(avatarService: AvatarServiceProtocol) {
|
||||
self.avatarService = avatarService
|
||||
}
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
func placeholderAvatar(matrixItemId: String,
|
||||
displayName: String?,
|
||||
colorCount: Int) -> AvatarViewState {
|
||||
let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName,
|
||||
matrixItemId: matrixItemId,
|
||||
colorCount: colorCount)
|
||||
|
||||
return .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex)
|
||||
}
|
||||
|
||||
/// Load an avatar
|
||||
/// - Parameters:
|
||||
/// - mxContentUri: The matrix content URI of the avatar.
|
||||
@@ -37,14 +49,10 @@ class AvatarViewModel: InjectableObject, ObservableObject {
|
||||
matrixItemId: String,
|
||||
displayName: String?,
|
||||
colorCount: Int,
|
||||
avatarSize: AvatarSize) {
|
||||
let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName,
|
||||
matrixItemId: matrixItemId,
|
||||
colorCount: colorCount)
|
||||
|
||||
viewState = .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex)
|
||||
|
||||
avatarSize: AvatarSize,
|
||||
avatarCompletion: @escaping (AvatarViewState) -> Void) {
|
||||
guard let mxContentUri = mxContentUri, mxContentUri.count > 0 else {
|
||||
avatarCompletion(placeholderAvatar(matrixItemId: matrixItemId, displayName: displayName, colorCount: colorCount))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -52,9 +60,16 @@ class AvatarViewModel: InjectableObject, ObservableObject {
|
||||
.sink { completion in
|
||||
guard case let .failure(error) = completion else { return }
|
||||
UILog.error("[AvatarService] Failed to retrieve avatar", context: error)
|
||||
// No need to call the completion, there's nothing we can do and the error is logged.
|
||||
} receiveValue: { image in
|
||||
self.viewState = .avatar(image)
|
||||
avatarCompletion(.avatar(image))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
extension AvatarViewModel {
|
||||
static func withMockedServices() -> AvatarViewModel {
|
||||
.init(avatarService: MockAvatarService.example)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// 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 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 {
|
||||
fatalError("No provider registered for type \(T.self)")
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var dependencies: DependencyContainer {
|
||||
get { self[DependencyContainerKey.self] }
|
||||
set { self[DependencyContainerKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
/// - 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//
|
||||
// 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 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>(_enclosingInstance instance: T,
|
||||
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
|
||||
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>) -> Value {
|
||||
get {
|
||||
// Resolve dependencies from enclosing instance's `dependencies` property
|
||||
let v: Value = instance.dependencies.resolve()
|
||||
return v
|
||||
}
|
||||
set {
|
||||
fatalError("Only subscript get is supported for injection")
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "This property wrapper can only be applied to classes")
|
||||
var wrappedValue: Value {
|
||||
get { fatalError("wrappedValue get not used") }
|
||||
set { fatalError("wrappedValue set not used. \(newValue)") }
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// 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 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.
|
||||
/// - Parameter dependencies: The `DependencyContainer` to inject.
|
||||
func inject(dependencies: DependencyContainer) {
|
||||
self.dependencies = dependencies
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Class that can be extended that supports injection and the `@Inject` property wrapper.
|
||||
open class InjectableObject: Injectable {
|
||||
var dependencies: DependencyContainer!
|
||||
}
|
||||
@@ -51,7 +51,7 @@ enum MockInfoSheetScreenState: MockScreenState, CaseIterable {
|
||||
return (
|
||||
[model, viewModel],
|
||||
AnyView(InfoSheet(viewModel: viewModel.context)
|
||||
.addDependency(MockAvatarService.example))
|
||||
.environmentObject(AvatarViewModel.withMockedServices()))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,8 @@ enum MockAppScreens {
|
||||
MockComposerScreenState.self,
|
||||
MockComposerCreateActionListScreenState.self,
|
||||
MockComposerLinkActionScreenState.self,
|
||||
MockVoiceBroadcastPlaybackScreenState.self
|
||||
MockVoiceBroadcastPlaybackScreenState.self,
|
||||
MockPollHistoryScreenState.self,
|
||||
MockPollHistoryDetailScreenState.self
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user