Add RTE formatting buttons (#1614)

* Add RTE formatting buttons

* Update UUID as stored properties

* Disable autocorrection on URL fields

* Alert ids to let

* Add ComposerToolbar_Previews

* Cleanup

* Cleanup

* Refactor FormatItem colors

* Fix composer layout issue

* Fix ui tests

* Nest ComposerToolbar A11y ids under RoomScreen

* Add composer reply ui tests

* Add UTs

---------

Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com>
This commit is contained in:
aringenbach
2023-09-05 17:39:54 +02:00
committed by GitHub
parent d8fa82da76
commit c9962a4459
47 changed files with 863 additions and 47 deletions
+13
View File
@@ -32,6 +32,19 @@ internal enum Asset {
internal static let appLogo = ImageAsset(name: "images/app-logo")
internal static let serverSelectionIcon = ImageAsset(name: "images/server-selection-icon")
internal static let closeCircle = ImageAsset(name: "images/close-circle")
internal static let bold = ImageAsset(name: "images/bold")
internal static let bulletList = ImageAsset(name: "images/bullet-list")
internal static let codeBlock = ImageAsset(name: "images/code-block")
internal static let indent = ImageAsset(name: "images/indent")
internal static let inlineCode = ImageAsset(name: "images/inline-code")
internal static let italic = ImageAsset(name: "images/italic")
internal static let link = ImageAsset(name: "images/link")
internal static let numberedList = ImageAsset(name: "images/numbered-list")
internal static let quote = ImageAsset(name: "images/quote")
internal static let strikethrough = ImageAsset(name: "images/strikethrough")
internal static let textFormat = ImageAsset(name: "images/text-format")
internal static let underline = ImageAsset(name: "images/underline")
internal static let unindent = ImageAsset(name: "images/unindent")
internal static let encryptionNormal = ImageAsset(name: "images/encryption-normal")
internal static let encryptionTrusted = ImageAsset(name: "images/encryption-trusted")
internal static let encryptionWarning = ImageAsset(name: "images/encryption-warning")
@@ -34,6 +34,7 @@ struct RoomProxyMockConfiguration {
var members: [RoomMemberProxyProtocol]?
var inviter: RoomMemberProxyMock?
var memberForID: RoomMemberProxyMock = .mockMe
var ownUserID = "@alice:somewhere.org"
var invitedMembersCount = 100
var joinedMembersCount = 50
@@ -61,6 +62,7 @@ extension RoomProxyMock {
invitedMembersCount = configuration.invitedMembersCount
joinedMembersCount = configuration.joinedMembersCount
activeMembersCount = configuration.activeMembersCount
ownUserID = configuration.ownUserID
if let members = configuration.members {
membersPublisher = Just(members).eraseToAnyPublisher()
@@ -78,5 +80,6 @@ extension RoomProxyMock {
setNameClosure = { _ in .success(()) }
setTopicClosure = { _ in .success(()) }
getMemberUserIDReturnValue = .success(configuration.memberForID)
canUserRedactUserIDReturnValue = .success(false)
}
}
@@ -106,12 +106,32 @@ struct A11yIdentifiers {
struct RoomScreen {
let name = "room-name"
let avatar = "room-avatar"
let attachmentPicker = "room-attachment_picker"
let attachmentPickerPhotoLibrary = "room-attachment_picker_photo_library"
let attachmentPickerDocuments = "room-attachment_picker_documents"
let attachmentPickerCamera = "room-attachment_picker_camera"
let attachmentPickerLocation = "room-attachment_picker_location"
let attachmentPickerPoll = "room-attachment_picker_poll"
let attachmentPickerTextFormatting = "room-attachment_picker_text_formatting"
let timelineItemActionMenu = "room-timeline_item_action_menu"
let composerToolbar = ComposerToolbar()
struct ComposerToolbar {
let bold = "composer_toolbar-bold"
let italic = "composer_toolbar-italic"
let underline = "composer_toolbar-underline"
let strikethrough = "composer_toolbar-strikethrough"
let unorderedList = "composer_toolbar-unordered_list"
let orderedList = "composer_toolbar-ordered_list"
let indent = "composer_toolbar-indent"
let unindent = "composer_toolbar-unindent"
let inlineCode = "composer_toolbar-inline_code"
let codeBlock = "composer_toolbar-code_block"
let quote = "composer_toolbar-quote"
let link = "composer_toolbar-link"
let openComposeOptions = "composer_toolbar-open_compose_options"
let closeFormattingOptions = "composer_toolbar-close-formatting-options"
}
}
struct RoomDetailsScreen {
+33 -1
View File
@@ -52,12 +52,21 @@ extension View {
/// .alert(item: $context.alertInfo)
/// ```
struct AlertInfo<T: Hashable>: Identifiable, AlertProtocol {
struct AlertButton {
struct AlertButton: Identifiable {
let id = UUID()
let title: String
var role: ButtonRole?
let action: (() -> Void)?
}
struct AlertTextField: Identifiable {
let id = UUID()
let placeholder: String
let text: Binding<String>
let autoCapitalization: TextInputAutocapitalization
let autoCorrectionDisabled: Bool
}
/// An identifier that can be used to distinguish one error from another.
let id: T
/// The alert's title.
@@ -68,6 +77,10 @@ struct AlertInfo<T: Hashable>: Identifiable, AlertProtocol {
var primaryButton = AlertButton(title: L10n.actionOk, action: nil)
/// The alert's secondary button title and action.
var secondaryButton: AlertButton?
/// The alert's displayed text fields.
var textFields: [AlertTextField]?
/// The alert's additional buttons displayed vertically above the primary button.
var verticalButtons: [AlertButton]?
}
extension AlertInfo {
@@ -95,9 +108,28 @@ extension AlertInfo {
extension View {
func alert<T: Hashable>(item: Binding<AlertInfo<T>?>) -> some View {
alert(item: item) { item in
if let verticalButtons = item.verticalButtons {
ForEach(verticalButtons) { button in
Button(button.title, role: button.role) {
button.action?()
}
}
}
if let textFields = item.textFields {
VStack(spacing: 24) {
ForEach(textFields) { textField in
TextField(textField.placeholder, text: textField.text)
.textInputAutocapitalization(textField.autoCapitalization)
.autocorrectionDisabled(textField.autoCorrectionDisabled)
}
}
}
Button(item.primaryButton.title, role: item.primaryButton.role) {
item.primaryButton.action?()
}
if let secondaryButton = item.secondaryButton {
Button(secondaryButton.title, role: secondaryButton.role) {
secondaryButton.action?()
@@ -14,7 +14,9 @@
// limitations under the License.
//
import SwiftUI
import UIKit
import WysiwygComposer
enum ComposerToolbarViewModelAction {
case sendMessage(plain: String, html: String, mode: RoomScreenComposerMode)
@@ -43,6 +45,8 @@ enum ComposerToolbarViewAction {
case displayLocationPicker
case displayPollForm
case handlePasteOrDrop(provider: NSItemProvider)
case enableTextFormatting
case composerAction(action: ComposerAction)
}
struct ComposerToolbarViewState: BindableState {
@@ -61,8 +65,11 @@ struct ComposerToolbarViewState: BindableState {
}
struct ComposerToolbarViewStateBindings {
var composerPlainText: String
var composerFocused: Bool
var composerPlainText = ""
var composerFocused = false
var composerActionsEnabled = false
var formatItems: [FormatItem] = .init()
var alertInfo: AlertInfo<UUID>?
var showAttachmentPopover = false {
didSet {
@@ -72,3 +79,167 @@ struct ComposerToolbarViewStateBindings {
}
}
}
/// An item in the toolbar
struct FormatItem {
/// The type of the item
let type: FormatType
/// The state of the item
let state: ActionState
}
/// The types of formatting actions
enum FormatType {
case bold
case italic
case underline
case strikeThrough
case unorderedList
case orderedList
case indent
case unindent
case inlineCode
case codeBlock
case quote
case link
}
extension FormatType: CaseIterable, Identifiable {
var id: Self { self }
}
extension FormatItem: Identifiable {
var id: FormatType { type }
}
extension FormatItem {
/// The icon to display in the formatting toolbar.
var icon: Image {
switch type {
case .bold:
return Image(asset: Asset.Images.bold)
case .italic:
return Image(asset: Asset.Images.italic)
case .underline:
return Image(asset: Asset.Images.underline)
case .strikeThrough:
return Image(asset: Asset.Images.strikethrough)
case .unorderedList:
return Image(asset: Asset.Images.bulletList)
case .orderedList:
return Image(asset: Asset.Images.numberedList)
case .indent:
return Image(asset: Asset.Images.indent)
case .unindent:
return Image(asset: Asset.Images.unindent)
case .inlineCode:
return Image(asset: Asset.Images.inlineCode)
case .codeBlock:
return Image(asset: Asset.Images.codeBlock)
case .quote:
return Image(asset: Asset.Images.quote)
case .link:
return Image(asset: Asset.Images.link)
}
}
var accessibilityIdentifier: String {
switch type {
case .bold:
return A11yIdentifiers.roomScreen.composerToolbar.bold
case .italic:
return A11yIdentifiers.roomScreen.composerToolbar.italic
case .underline:
return A11yIdentifiers.roomScreen.composerToolbar.underline
case .strikeThrough:
return A11yIdentifiers.roomScreen.composerToolbar.strikethrough
case .unorderedList:
return A11yIdentifiers.roomScreen.composerToolbar.unorderedList
case .orderedList:
return A11yIdentifiers.roomScreen.composerToolbar.orderedList
case .indent:
return A11yIdentifiers.roomScreen.composerToolbar.indent
case .unindent:
return A11yIdentifiers.roomScreen.composerToolbar.unindent
case .inlineCode:
return A11yIdentifiers.roomScreen.composerToolbar.inlineCode
case .codeBlock:
return A11yIdentifiers.roomScreen.composerToolbar.codeBlock
case .quote:
return A11yIdentifiers.roomScreen.composerToolbar.quote
case .link:
return A11yIdentifiers.roomScreen.composerToolbar.link
}
}
var accessibilityLabel: String {
switch type {
case .bold:
return L10n.richTextEditorFormatBold
case .italic:
return L10n.richTextEditorFormatItalic
case .underline:
return L10n.richTextEditorFormatUnderline
case .strikeThrough:
return L10n.richTextEditorFormatStrikethrough
case .unorderedList:
return L10n.richTextEditorBulletList
case .orderedList:
return L10n.richTextEditorNumberedList
case .indent:
return L10n.richTextEditorIndent
case .unindent:
return L10n.richTextEditorUnindent
case .inlineCode:
return L10n.richTextEditorInlineCode
case .codeBlock:
return L10n.richTextEditorCodeBlock
case .quote:
return L10n.richTextEditorQuote
case .link:
return L10n.richTextEditorLink
}
}
}
extension FormatType {
/// The associated library composer action.
var composerAction: ComposerAction {
switch self {
case .bold:
return .bold
case .italic:
return .italic
case .underline:
return .underline
case .strikeThrough:
return .strikeThrough
case .unorderedList:
return .unorderedList
case .orderedList:
return .orderedList
case .indent:
return .indent
case .unindent:
return .unindent
case .inlineCode:
return .inlineCode
case .codeBlock:
return .codeBlock
case .quote:
return .quote
case .link:
return .link
}
}
/// Return true if the format type is an indentation action.
var isIndentType: Bool {
switch self {
case .indent, .unindent:
return true
default:
return false
}
}
}
@@ -15,6 +15,8 @@
//
import Combine
import Foundation
import SwiftUI
import WysiwygComposer
typealias ComposerToolbarViewModelType = StateStoreViewModel<ComposerToolbarViewState, ComposerToolbarViewAction>
@@ -26,10 +28,19 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
actionsSubject.eraseToAnyPublisher()
}
private struct WysiwygLinkData {
let action: LinkAction
let range: NSRange
var url: String
var text: String
}
private var currentLinkData: WysiwygLinkData?
init(wysiwygViewModel: WysiwygComposerViewModel) {
self.wysiwygViewModel = wysiwygViewModel
super.init(initialViewState: ComposerToolbarViewState(bindings: .init(composerPlainText: "", composerFocused: false)))
super.init(initialViewState: ComposerToolbarViewState(bindings: .init()))
context.$viewState
.map(\.composerMode)
@@ -46,6 +57,20 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
wysiwygViewModel.$isContentEmpty
.weakAssign(to: \.state.composerEmpty, on: self)
.store(in: &cancellables)
wysiwygViewModel.$actionStates
.map { actions in
FormatType
.allCases
// Exclude indent type outside of lists.
.filter { wysiwygViewModel.isInList || !$0.isIndentType }
.map { type in
FormatItem(type: type,
state: actions[type.composerAction] ?? .disabled)
}
}
.weakAssign(to: \.state.bindings.formatItems, on: self)
.store(in: &cancellables)
}
// MARK: - Public
@@ -65,6 +90,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
actionsSubject.send(.sendPlainTextMessage(message: context.composerPlainText,
mode: state.composerMode))
}
state.bindings.composerActionsEnabled = false
case .cancelReply:
set(mode: .default)
case .cancelEdit:
@@ -82,6 +108,15 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
actionsSubject.send(.displayPollForm)
case .handlePasteOrDrop(let provider):
actionsSubject.send(.handlePasteOrDrop(provider: provider))
case .enableTextFormatting:
state.bindings.composerActionsEnabled = true
state.bindings.composerFocused = true
case .composerAction(let action):
if action == .link {
createLinkAlert()
} else {
wysiwygViewModel.apply(action)
}
}
}
@@ -128,4 +163,124 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
state.bindings.composerPlainText = text
}
}
private func createLinkAlert() {
let linkAction = wysiwygViewModel.getLinkAction()
currentLinkData = WysiwygLinkData(action: linkAction,
range: wysiwygViewModel.attributedContent.selection,
url: linkAction.url ?? "",
text: "")
let urlBinding: Binding<String> = .init { [weak self] in
self?.currentLinkData?.url ?? ""
} set: { [weak self] value in
self?.currentLinkData?.url = value
}
let textBinding: Binding<String> = .init { [weak self] in
self?.currentLinkData?.text ?? ""
} set: { [weak self] value in
self?.currentLinkData?.text = value
}
switch linkAction {
case .createWithText:
state.bindings.alertInfo = makeCreateWithTextAlertInfo(urlBinding: urlBinding, textBinding: textBinding)
case .create:
state.bindings.alertInfo = makeSetUrlAlertInfo(urlBinding: urlBinding, isEdit: false)
case .edit:
state.bindings.alertInfo = makeEditChoiceAlertInfo(urlBinding: urlBinding)
case .disabled:
break
}
}
private func makeCreateWithTextAlertInfo(urlBinding: Binding<String>, textBinding: Binding<String>) -> AlertInfo<UUID> {
AlertInfo(id: UUID(),
title: L10n.richTextEditorCreateLink,
primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionCancel, action: {
self.restoreComposerSelectedRange()
}),
secondaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionSave, action: {
self.restoreComposerSelectedRange()
self.createLinkWithText()
}),
textFields: [AlertInfo<UUID>.AlertTextField(placeholder: L10n.commonText,
text: textBinding,
autoCapitalization: .never,
autoCorrectionDisabled: false),
AlertInfo<UUID>.AlertTextField(placeholder: L10n.richTextEditorUrlPlaceholder,
text: urlBinding,
autoCapitalization: .never,
autoCorrectionDisabled: true)])
}
private func makeSetUrlAlertInfo(urlBinding: Binding<String>, isEdit: Bool) -> AlertInfo<UUID> {
AlertInfo(id: UUID(),
title: isEdit ? L10n.richTextEditorEditLink : L10n.richTextEditorCreateLink,
primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionCancel, action: {
self.restoreComposerSelectedRange()
}),
secondaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionSave, action: {
self.restoreComposerSelectedRange()
self.setLink()
}),
textFields: [AlertInfo<UUID>.AlertTextField(placeholder: L10n.richTextEditorUrlPlaceholder,
text: urlBinding,
autoCapitalization: .never,
autoCorrectionDisabled: true)])
}
private func makeEditChoiceAlertInfo(urlBinding: Binding<String>) -> AlertInfo<UUID> {
AlertInfo(id: UUID(),
title: L10n.richTextEditorEditLink,
primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionRemove, role: .destructive, action: {
self.restoreComposerSelectedRange()
self.removeLinks()
}),
verticalButtons: [AlertInfo<UUID>.AlertButton(title: L10n.actionEdit, action: {
self.state.bindings.alertInfo = nil
DispatchQueue.main.async {
self.state.bindings.alertInfo = self.makeSetUrlAlertInfo(urlBinding: urlBinding, isEdit: true)
}
})])
}
private func restoreComposerSelectedRange() {
guard let currentLinkData else { return }
wysiwygViewModel.select(range: currentLinkData.range)
}
private func setLink() {
guard let currentLinkData else { return }
wysiwygViewModel.applyLinkOperation(.setLink(urlString: currentLinkData.url))
}
private func createLinkWithText() {
guard let currentLinkData else { return }
wysiwygViewModel.applyLinkOperation(.createLink(urlString: currentLinkData.url,
text: currentLinkData.text))
}
private func removeLinks() {
wysiwygViewModel.applyLinkOperation(.removeLinks)
}
}
private extension WysiwygComposerViewModel {
/// Return true if the selection of the composer is currently located in a list.
var isInList: Bool {
actionStates[.orderedList] == .reversed || actionStates[.unorderedList] == .reversed
}
}
private extension LinkAction {
var url: String? {
guard case .edit(let url) = self else {
return nil
}
return url
}
}
@@ -26,29 +26,65 @@ struct ComposerToolbar: View {
@ScaledMetric private var sendButtonIconSize = 16
var body: some View {
VStack(spacing: 8) {
topBar
if context.composerActionsEnabled {
bottomBar
}
}
.alert(item: $context.alertInfo)
}
private var topBar: some View {
HStack(alignment: .bottom, spacing: 10) {
RoomAttachmentPicker(context: context)
.padding(.bottom, 5) // centre align with the send button
if !context.composerActionsEnabled {
RoomAttachmentPicker(context: context)
.padding(.bottom, 5) // centre align with the send button
}
messageComposer
.environmentObject(context)
Button {
context.send(viewAction: .sendMessage)
} label: {
submitButtonImage
.symbolVariant(.fill)
.font(.compound.bodyLG)
.foregroundColor(context.viewState.sendButtonDisabled ? .compound.iconDisabled : .global.white)
.background {
Circle()
.foregroundColor(context.viewState.sendButtonDisabled ? .clear : .compound.iconAccentTertiary)
}
if !context.composerActionsEnabled {
sendButton
}
.disabled(context.viewState.sendButtonDisabled)
.animation(.linear(duration: 0.1), value: context.viewState.sendButtonDisabled)
.keyboardShortcut(.return, modifiers: [.command])
.padding([.vertical, .trailing], 6)
}
}
private var bottomBar: some View {
HStack(alignment: .bottom, spacing: 10) {
Button {
context.composerActionsEnabled = false
} label: {
Image(systemName: "xmark.circle.fill")
.font(.compound.headingLG)
.foregroundColor(.compound.textActionPrimary)
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions)
.padding(.bottom, 5) // centre align with the send button
FormattingToolbar(formatItems: context.formatItems) { action in
context.send(viewAction: .composerAction(action: action.composerAction))
}
sendButton
}
}
private var sendButton: some View {
Button {
context.send(viewAction: .sendMessage)
} label: {
submitButtonImage
.symbolVariant(.fill)
.font(.compound.bodyLG)
.foregroundColor(context.viewState.sendButtonDisabled ? .compound.iconDisabled : .global.white)
.background {
Circle()
.foregroundColor(context.viewState.sendButtonDisabled ? .clear : .compound.iconAccentTertiary)
}
}
.disabled(context.viewState.sendButtonDisabled)
.animation(.linear(duration: 0.1), value: context.viewState.sendButtonDisabled)
.keyboardShortcut(.return, modifiers: [.command])
.padding([.vertical, .trailing], 6)
}
private var messageComposer: some View {
MessageComposer(plainText: $context.composerPlainText,
@@ -109,6 +145,12 @@ struct ComposerToolbar: View {
}
}
struct ComposerToolbar_Previews: PreviewProvider {
static var previews: some View {
ComposerToolbar.mock()
}
}
// MARK: - Mock
extension ComposerToolbar {
@@ -0,0 +1,68 @@
//
// Copyright 2023 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
struct FormattingToolbar: View {
/// The list of items to render in the toolbar
var formatItems: [FormatItem]
/// The action when an item is selected
var formatAction: (FormatType) -> Void
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 4) {
ForEach(formatItems) { item in
Button {
formatAction(item.type)
} label: {
item.icon
.renderingMode(.template)
.foregroundColor(item.foregroundColor)
}
.disabled(item.state == .disabled)
.frame(width: 44, height: 44)
.background(item.backgroundColor)
.cornerRadius(8)
.accessibilityIdentifier(item.accessibilityIdentifier)
.accessibilityLabel(item.accessibilityLabel)
}
}
}
}
}
private extension FormatItem {
var foregroundColor: Color {
switch state {
case .reversed:
return .compound.iconOnSolidPrimary
case .enabled:
return .compound.iconSecondary
case .disabled:
return .compound.iconDisabled
}
}
var backgroundColor: Color {
switch state {
case .reversed:
return .compound.bgActionPrimaryRest
default:
return .compound.bgCanvasDefault
}
}
}
@@ -59,7 +59,7 @@ struct MessageComposer: View {
}
}
}
.padding([.leading, .trailing], 12.0)
.padding(.horizontal, 12.0)
.clipped()
.background {
ZStack {
@@ -107,16 +107,16 @@ private struct MessageComposerReplyHeader: View {
.padding(4.0)
.background(Color.compound.bgCanvasDefault)
.cornerRadius(13.0)
.padding([.trailing, .vertical], 8.0)
.padding([.leading], -4.0)
.overlay(alignment: .topTrailing) {
Button(action: action) {
Image(systemName: "xmark")
.font(.compound.bodySM.weight(.medium))
.foregroundColor(.compound.iconTertiary)
.padding(16.0)
.padding(8.0)
}
}
.padding(.vertical, 8.0)
.padding(.horizontal, -4.0)
}
}
@@ -31,7 +31,7 @@ struct RoomAttachmentPicker: View {
.font(.compound.headingLG)
.foregroundColor(.compound.textActionPrimary)
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPicker)
.accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions)
.popover(isPresented: $context.showAttachmentPopover) {
VStack(alignment: .leading, spacing: 0.0) {
Button {
@@ -72,6 +72,17 @@ struct RoomAttachmentPicker: View {
} label: {
PickerLabel(title: L10n.screenRoomAttachmentSourcePoll, icon: Image(asset: Asset.Images.timelinePollAttachment))
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerPoll)
if ServiceLocator.shared.settings.richTextEditorEnabled {
Button {
context.showAttachmentPopover = false
context.send(viewAction: .enableTextFormatting)
} label: {
PickerLabel(title: L10n.screenRoomAttachmentTextFormatting, icon: Image(asset: Asset.Images.textFormat))
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerTextFormatting)
}
}
.padding(.top, isPresented ? 20 : 0)
.background {
@@ -368,7 +368,7 @@ class MockScreen: Identifiable {
var sessionVerificationControllerProxy = SessionVerificationControllerProxyMock.configureMock(requestDelay: .seconds(5))
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy)
return SessionVerificationScreenCoordinator(parameters: parameters)
case .userSessionScreen:
case .userSessionScreen, .userSessionScreenReply:
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator())
let clientProxy = MockClientProxy(userID: "@mock:client.com", roomSummaryProvider: MockRoomSummaryProvider(state: .loaded(.mockRooms)))
@@ -50,6 +50,7 @@ enum UITestsScreenIdentifier: String {
case roomWithUndisclosedPolls
case sessionVerification
case userSessionScreen
case userSessionScreenReply
case roomDetailsScreen
case roomDetailsScreenWithRoomAvatar
case roomDetailsScreenWithEmptyTopic