mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-26 11:30:50 +02:00
Display user suggestion list in fullscreen mode with shared context from UserSuggestionCoordinator
This commit is contained in:
@@ -29,12 +29,24 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: ComposerViewModel
|
||||
let userSuggestionViewModel = MockUserSuggestionViewModel(initialViewState: UserSuggestionViewState(items: []))
|
||||
let userSuggestionSharedContext = UserSuggestionSharedContext(context: userSuggestionViewModel.context,
|
||||
mediaManager: MXMediaManager())
|
||||
let bindings = ComposerBindings(focused: false)
|
||||
|
||||
switch self {
|
||||
case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings))
|
||||
case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit, textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings))
|
||||
case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply, textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings))
|
||||
case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true,
|
||||
isLandscapePhone: false,
|
||||
bindings: bindings))
|
||||
case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit,
|
||||
textFormattingEnabled: true,
|
||||
isLandscapePhone: false,
|
||||
bindings: bindings))
|
||||
case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser",
|
||||
sendMode: .reply,
|
||||
textFormattingEnabled: true,
|
||||
isLandscapePhone: false,
|
||||
bindings: bindings))
|
||||
}
|
||||
|
||||
let wysiwygviewModel = WysiwygComposerViewModel(minHeight: 20, maxCompressedHeight: 360)
|
||||
@@ -57,6 +69,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
|
||||
Spacer()
|
||||
Composer(viewModel: viewModel.context,
|
||||
wysiwygViewModel: wysiwygviewModel,
|
||||
userSuggestionSharedContext: userSuggestionSharedContext,
|
||||
resizeAnimationDuration: 0.1,
|
||||
sendMessageAction: { _ in },
|
||||
showSendMediaActions: { })
|
||||
@@ -70,3 +83,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private final class MockUserSuggestionViewModel: UserSuggestionViewModelType {
|
||||
|
||||
}
|
||||
|
||||
@@ -256,3 +256,12 @@ final class SuggestionPatternWrapper: NSObject {
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
final class UserSuggestionViewModelWrapper: NSObject {
|
||||
let userSuggestionViewModel: UserSuggestionViewModel
|
||||
|
||||
init(_ userSuggestionViewModel: UserSuggestionViewModel) {
|
||||
self.userSuggestionViewModel = userSuggestionViewModel
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ struct Composer: View {
|
||||
// MARK: Private
|
||||
@ObservedObject private var viewModel: ComposerViewModelType.Context
|
||||
@ObservedObject private var wysiwygViewModel: WysiwygComposerViewModel
|
||||
private let userSuggestionSharedContext: UserSuggestionSharedContext
|
||||
private let resizeAnimationDuration: Double
|
||||
|
||||
private let sendMessageAction: (WysiwygComposerContent) -> Void
|
||||
@@ -31,15 +32,42 @@ struct Composer: View {
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
@State private var isActionButtonShowing = false
|
||||
|
||||
|
||||
private let horizontalPadding: CGFloat = 12
|
||||
private let borderHeight: CGFloat = 40
|
||||
private var verticalPadding: CGFloat {
|
||||
private let standardVerticalPadding: CGFloat = 8.0
|
||||
private let contextBannerHeight: CGFloat = 14.5
|
||||
|
||||
/// Spacing applied within the VStack holding the context banner and the composer text view.
|
||||
private let verticalComponentSpacing: CGFloat = 12.0
|
||||
/// Padding for the main composer text view. Always applied on bottom.
|
||||
/// Applied on top only if no context banner is present.
|
||||
private var composerVerticalPadding: CGFloat {
|
||||
(borderHeight - wysiwygViewModel.minHeight) / 2
|
||||
}
|
||||
|
||||
private var topPadding: CGFloat {
|
||||
viewModel.viewState.shouldDisplayContext ? 0 : verticalPadding
|
||||
|
||||
/// Computes the top padding to apply on the composer text view depending on context.
|
||||
private var composerTopPadding: CGFloat {
|
||||
viewModel.viewState.shouldDisplayContext ? 0 : composerVerticalPadding
|
||||
}
|
||||
|
||||
/// Computes the additional height required to display the context banner.
|
||||
/// Returns 0.0 if the banner is not displayed.
|
||||
/// Note: height of the actual banner + its added standard top padding + VStack spacing
|
||||
private var additionalHeightForContextBanner: CGFloat {
|
||||
viewModel.viewState.shouldDisplayContext ? contextBannerHeight + standardVerticalPadding + verticalComponentSpacing : 0
|
||||
}
|
||||
|
||||
/// Computes the total height of the composer (excluding the RTE formatting bar).
|
||||
/// This height includes the text view, as well as the context banner
|
||||
/// and user suggestion list when displayed.
|
||||
private var composerHeight: CGFloat {
|
||||
wysiwygViewModel.idealHeight
|
||||
+ composerTopPadding
|
||||
+ composerVerticalPadding
|
||||
// Extra padding added on top of the VStack containing the composer
|
||||
+ standardVerticalPadding
|
||||
+ additionalHeightForContextBanner
|
||||
}
|
||||
|
||||
private var cornerRadius: CGFloat {
|
||||
@@ -84,7 +112,7 @@ struct Composer: View {
|
||||
|
||||
private var composerContainer: some View {
|
||||
let rect = RoundedRectangle(cornerRadius: cornerRadius)
|
||||
return VStack(spacing: 12) {
|
||||
return VStack(spacing: verticalComponentSpacing) {
|
||||
if viewModel.viewState.shouldDisplayContext {
|
||||
HStack {
|
||||
if let imageName = viewModel.viewState.contextImageName {
|
||||
@@ -106,7 +134,8 @@ struct Composer: View {
|
||||
}
|
||||
.accessibilityIdentifier("cancelButton")
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.frame(height: contextBannerHeight)
|
||||
.padding(.top, standardVerticalPadding)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
}
|
||||
HStack(alignment: shouldFixRoundCorner ? .top : .center, spacing: 0) {
|
||||
@@ -116,7 +145,6 @@ struct Composer: View {
|
||||
)
|
||||
.tintColor(theme.colors.accent)
|
||||
.placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent)
|
||||
.frame(height: wysiwygViewModel.idealHeight)
|
||||
.onAppear {
|
||||
if wysiwygViewModel.isContentEmpty {
|
||||
wysiwygViewModel.setup()
|
||||
@@ -137,13 +165,13 @@ struct Composer: View {
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.top, topPadding)
|
||||
.padding(.bottom, verticalPadding)
|
||||
.padding(.top, composerTopPadding)
|
||||
.padding(.bottom, composerVerticalPadding)
|
||||
}
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(borderColor, lineWidth: 1))
|
||||
.animation(.easeInOut(duration: resizeAnimationDuration), value: wysiwygViewModel.idealHeight)
|
||||
.padding(.top, 8)
|
||||
.padding(.top, standardVerticalPadding)
|
||||
.onTapGesture {
|
||||
if viewModel.focused {
|
||||
viewModel.focused = true
|
||||
@@ -195,11 +223,13 @@ struct Composer: View {
|
||||
init(
|
||||
viewModel: ComposerViewModelType.Context,
|
||||
wysiwygViewModel: WysiwygComposerViewModel,
|
||||
userSuggestionSharedContext: UserSuggestionSharedContext,
|
||||
resizeAnimationDuration: Double,
|
||||
sendMessageAction: @escaping (WysiwygComposerContent) -> Void,
|
||||
showSendMediaActions: @escaping () -> Void) {
|
||||
self.viewModel = viewModel
|
||||
self.wysiwygViewModel = wysiwygViewModel
|
||||
self.userSuggestionSharedContext = userSuggestionSharedContext
|
||||
self.resizeAnimationDuration = resizeAnimationDuration
|
||||
self.sendMessageAction = sendMessageAction
|
||||
self.showSendMediaActions = showSendMediaActions
|
||||
@@ -213,17 +243,24 @@ struct Composer: View {
|
||||
.frame(width: 36, height: 5)
|
||||
.padding(.top, 10)
|
||||
}
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
if !viewModel.viewState.textFormattingEnabled {
|
||||
sendMediaButton
|
||||
.padding(.bottom, 1)
|
||||
VStack {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
if !viewModel.viewState.textFormattingEnabled {
|
||||
sendMediaButton
|
||||
.padding(.bottom, 1)
|
||||
}
|
||||
composerContainer
|
||||
if !viewModel.viewState.textFormattingEnabled {
|
||||
sendButton
|
||||
.padding(.bottom, 1)
|
||||
}
|
||||
}
|
||||
composerContainer
|
||||
if !viewModel.viewState.textFormattingEnabled {
|
||||
sendButton
|
||||
.padding(.bottom, 1)
|
||||
if wysiwygViewModel.maximised {
|
||||
UserSuggestionList(viewModel: userSuggestionSharedContext.context)
|
||||
.environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: userSuggestionSharedContext.mediaManager)))
|
||||
}
|
||||
}
|
||||
.frame(height: composerHeight)
|
||||
if viewModel.viewState.textFormattingEnabled {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
sendMediaButton
|
||||
|
||||
@@ -30,6 +30,19 @@ struct UserSuggestionCoordinatorParameters {
|
||||
let room: MXRoom
|
||||
}
|
||||
|
||||
/// Defines a shared context providing the ability to use a single `UserSuggestionViewModel` for multiple
|
||||
/// `UserSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController`
|
||||
/// UIKit hosted context, and in Rich-Text-Editor's SwiftUI fullscreen mode, without need to reload data.
|
||||
final class UserSuggestionSharedContext: NSObject {
|
||||
let context: UserSuggestionViewModelType.Context
|
||||
let mediaManager: MXMediaManager
|
||||
|
||||
init(context: UserSuggestionViewModelType.Context, mediaManager: MXMediaManager) {
|
||||
self.context = context
|
||||
self.mediaManager = mediaManager
|
||||
}
|
||||
}
|
||||
|
||||
final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
@@ -105,6 +118,11 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
userSuggestionHostingController
|
||||
}
|
||||
|
||||
func sharedContext() -> UserSuggestionSharedContext {
|
||||
UserSuggestionSharedContext(context: userSuggestionViewModel.sharedContext,
|
||||
mediaManager: parameters.mediaManager)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func calculateViewHeight() -> CGFloat {
|
||||
|
||||
+4
@@ -52,6 +52,10 @@ final class UserSuggestionCoordinatorBridge: NSObject {
|
||||
func toPresentable() -> UIViewController? {
|
||||
userSuggestionCoordinator.toPresentable()
|
||||
}
|
||||
|
||||
func sharedContext() -> UserSuggestionSharedContext {
|
||||
userSuggestionCoordinator.sharedContext()
|
||||
}
|
||||
}
|
||||
|
||||
extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate {
|
||||
|
||||
@@ -27,7 +27,11 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo
|
||||
private let userSuggestionService: UserSuggestionServiceProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
|
||||
var sharedContext: UserSuggestionViewModelType.Context {
|
||||
return self.context
|
||||
}
|
||||
|
||||
var completion: ((UserSuggestionViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -17,5 +17,6 @@
|
||||
import Foundation
|
||||
|
||||
protocol UserSuggestionViewModelProtocol {
|
||||
var sharedContext: UserSuggestionViewModelType.Context { get }
|
||||
var completion: ((UserSuggestionViewModelResult) -> Void)? { get set }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user