mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 03:48:27 +00:00
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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user