mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Composer and RTE polish (#1719)
* Move link button * Always show indent buttons * Fix code blocks and quote block styles * Fix code blocks and quotes style * Update composer attachment icon * Fix send message icon * Fix composer border * Fix rte border * Update RTE button assets * Update close rte icon * Refactor RTE and composer layout * more padding fixes * Update compound * Update WysiwygComposer to 2.11.0 * Fix ui tests * Fix composer bottom padding * Update snapshot tests
This commit is contained in:
@@ -55,6 +55,8 @@ internal enum Asset {
|
||||
internal static let locationPin = ImageAsset(name: "images/location-pin")
|
||||
internal static let locationPointerFull = ImageAsset(name: "images/location-pointer-full")
|
||||
internal static let locationPointer = ImageAsset(name: "images/location-pointer")
|
||||
internal static let closeRte = ImageAsset(name: "images/close-rte")
|
||||
internal static let composerAttachment = ImageAsset(name: "images/composer-attachment")
|
||||
internal static let timelineComposerSendMessage = ImageAsset(name: "images/timeline-composer-send-message")
|
||||
internal static let timelineEndedPoll = ImageAsset(name: "images/timeline-ended-poll")
|
||||
internal static let timelinePollAttachment = ImageAsset(name: "images/timeline-poll-attachment")
|
||||
|
||||
@@ -139,7 +139,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
attributedString.enumerateAttribute(.backgroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
|
||||
if let value = value as? UIColor,
|
||||
value == temporaryCodeBlockMarkingColor {
|
||||
attributedString.addAttribute(.backgroundColor, value: UIColor.compound._bgCodeBlock as Any, range: range)
|
||||
attributedString.addAttribute(.backgroundColor, value: UIColor(.compound._bgCodeBlock) as Any, range: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ enum FormatType {
|
||||
case italic
|
||||
case underline
|
||||
case strikeThrough
|
||||
case link
|
||||
case unorderedList
|
||||
case orderedList
|
||||
case indent
|
||||
@@ -95,7 +96,6 @@ enum FormatType {
|
||||
case inlineCode
|
||||
case codeBlock
|
||||
case quote
|
||||
case link
|
||||
}
|
||||
|
||||
extension FormatType: CaseIterable, Identifiable {
|
||||
@@ -226,14 +226,4 @@ extension FormatType {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +62,6 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
.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)
|
||||
@@ -259,13 +257,6 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -24,6 +24,7 @@ struct ComposerToolbar: View {
|
||||
|
||||
@FocusState private var composerFocused: Bool
|
||||
@ScaledMetric private var sendButtonIconSize = 16
|
||||
@ScaledMetric(relativeTo: .title) private var closeRTEButtonSize = 30
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
@@ -36,25 +37,29 @@ struct ComposerToolbar: View {
|
||||
}
|
||||
|
||||
private var topBar: some View {
|
||||
HStack(alignment: .bottom, spacing: 10) {
|
||||
HStack(alignment: .bottom, spacing: 5) {
|
||||
if !context.composerActionsEnabled {
|
||||
RoomAttachmentPicker(context: context)
|
||||
.padding(.bottom, 5) // centre align with the send button
|
||||
}
|
||||
|
||||
messageComposer
|
||||
.environmentObject(context)
|
||||
.onTapGesture {
|
||||
guard !composerFocused else { return }
|
||||
composerFocused = true
|
||||
}
|
||||
.padding(.leading, context.composerActionsEnabled ? 7 : 0)
|
||||
.padding(.trailing, context.composerActionsEnabled ? 4 : 0)
|
||||
|
||||
if !context.composerActionsEnabled {
|
||||
sendButton
|
||||
.padding(.leading, 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var bottomBar: some View {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
HStack(alignment: .center, spacing: 9) {
|
||||
closeRTEButton
|
||||
|
||||
FormattingToolbar(formatItems: context.formatItems) { action in
|
||||
@@ -62,6 +67,7 @@ struct ComposerToolbar: View {
|
||||
}
|
||||
|
||||
sendButton
|
||||
.padding(.leading, 7)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,9 +76,11 @@ struct ComposerToolbar: View {
|
||||
context.composerActionsEnabled = false
|
||||
context.composerExpanded = false
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.compound.headingLG)
|
||||
.foregroundColor(.compound.textActionPrimary)
|
||||
Image(Asset.Images.closeRte.name)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: closeRTEButtonSize, height: closeRTEButtonSize)
|
||||
.padding(7)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions)
|
||||
}
|
||||
@@ -89,11 +97,11 @@ struct ComposerToolbar: View {
|
||||
Circle()
|
||||
.foregroundColor(context.viewState.sendButtonDisabled ? .clear : .compound.iconAccentTertiary)
|
||||
}
|
||||
.padding(4)
|
||||
}
|
||||
.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 {
|
||||
@@ -133,6 +141,7 @@ struct ComposerToolbar: View {
|
||||
|
||||
private var composerView: WysiwygComposerView {
|
||||
WysiwygComposerView(placeholder: placeholder,
|
||||
placeholderColor: .compound.textSecondary,
|
||||
viewModel: wysiwygViewModel,
|
||||
itemProviderHelper: ItemProviderHelper(),
|
||||
keyCommandHandler: keyCommandHandler) { provider in
|
||||
@@ -151,7 +160,7 @@ struct ComposerToolbar: View {
|
||||
Image(asset: Asset.Images.timelineComposerSendMessage)
|
||||
.resizable()
|
||||
.frame(width: sendButtonIconSize, height: sendButtonIconSize)
|
||||
.padding(EdgeInsets(top: 7, leading: 8, bottom: 7, trailing: 6))
|
||||
.padding(EdgeInsets(top: 10, leading: 11, bottom: 10, trailing: 9))
|
||||
.opacity(context.viewState.composerMode.isEdit ? 0 : 1)
|
||||
.accessibilityLabel(L10n.actionSend)
|
||||
.accessibilityHidden(context.viewState.composerMode.isEdit)
|
||||
|
||||
@@ -22,7 +22,7 @@ struct FormattingToolbar: View {
|
||||
/// The action when an item is selected
|
||||
var formatAction: (FormatType) -> Void
|
||||
|
||||
@ScaledMetric private var toolbarButtonIconSize = 24
|
||||
@ScaledMetric private var toolbarButtonIconSize = 20
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal) {
|
||||
@@ -32,16 +32,16 @@ struct FormattingToolbar: View {
|
||||
formatAction(item.type)
|
||||
} label: {
|
||||
item.icon
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: toolbarButtonIconSize, height: toolbarButtonIconSize)
|
||||
.foregroundColor(item.foregroundColor)
|
||||
.padding(10)
|
||||
.padding(8)
|
||||
.background(item.backgroundColor)
|
||||
.cornerRadius(8)
|
||||
.padding(4)
|
||||
}
|
||||
.disabled(item.state == .disabled)
|
||||
.background(item.backgroundColor)
|
||||
.cornerRadius(8)
|
||||
.accessibilityIdentifier(item.accessibilityIdentifier)
|
||||
.accessibilityLabel(item.accessibilityLabel)
|
||||
}
|
||||
@@ -54,7 +54,7 @@ private extension FormatItem {
|
||||
var foregroundColor: Color {
|
||||
switch state {
|
||||
case .reversed:
|
||||
return .compound.iconOnSolidPrimary
|
||||
return .compound.iconSuccessPrimary
|
||||
case .enabled:
|
||||
return .compound.iconSecondary
|
||||
case .disabled:
|
||||
@@ -65,8 +65,8 @@ private extension FormatItem {
|
||||
var backgroundColor: Color {
|
||||
switch state {
|
||||
case .reversed:
|
||||
return .compound.bgActionPrimaryRest
|
||||
default:
|
||||
return .compound._bgAccentSelected
|
||||
case .enabled, .disabled:
|
||||
return .compound.bgCanvasDefault
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,7 @@ struct MessageComposer: View {
|
||||
roundedRectangle
|
||||
.fill(Color.compound.bgSubtleSecondary)
|
||||
roundedRectangle
|
||||
.stroke(Color.compound._borderTextFieldFocused, lineWidth: 1)
|
||||
.opacity(focused ? 1 : 0)
|
||||
.stroke(Color.compound._borderTextFieldFocused, lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
// Explicitly disable all animations to fix weirdness with the header immediately
|
||||
|
||||
@@ -20,16 +20,20 @@ import WysiwygComposer
|
||||
struct RoomAttachmentPicker: View {
|
||||
@ObservedObject var context: ComposerToolbarViewModel.Context
|
||||
@Environment(\.isPresented) var isPresented
|
||||
|
||||
@ScaledMetric(relativeTo: .title) private var attachmentButtonSize: CGFloat = 30
|
||||
|
||||
@State private var sheetContentHeight = CGFloat(0)
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
context.showAttachmentPopover = true
|
||||
} label: {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.compound.headingLG)
|
||||
Image(Asset.Images.composerAttachment.name)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: attachmentButtonSize, height: attachmentButtonSize)
|
||||
.foregroundColor(.compound.textActionPrimary)
|
||||
.padding(7)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions)
|
||||
.popover(isPresented: $context.showAttachmentPopover) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import HTMLParser
|
||||
import SwiftUI
|
||||
import WysiwygComposer
|
||||
|
||||
@@ -63,7 +64,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
||||
maxCompressedHeight: ComposerConstant.maxHeight,
|
||||
maxExpandedHeight: ComposerConstant.maxHeight)
|
||||
maxExpandedHeight: ComposerConstant.maxHeight,
|
||||
parserStyle: .elementX)
|
||||
composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel)
|
||||
}
|
||||
|
||||
@@ -133,3 +135,20 @@ enum ComposerConstant {
|
||||
static let allowedHeightRange = minHeight...maxHeight
|
||||
static let translationThreshold: CGFloat = 60
|
||||
}
|
||||
|
||||
private extension HTMLParserStyle {
|
||||
static let elementX = HTMLParserStyle(textColor: UIColor.label,
|
||||
linkColor: UIColor.link,
|
||||
codeBlockStyle: BlockStyle(backgroundColor: UIColor(.compound._bgCodeBlock),
|
||||
borderColor: UIColor(.compound.borderInteractiveSecondary),
|
||||
borderWidth: 0.0,
|
||||
cornerRadius: 0.0,
|
||||
padding: BlockStyle.Padding(horizontal: 10, vertical: 12),
|
||||
type: .background),
|
||||
quoteBlockStyle: BlockStyle(backgroundColor: UIColor(.compound.iconTertiary),
|
||||
borderColor: UIColor(.compound.borderInteractiveSecondary),
|
||||
borderWidth: 0.0,
|
||||
cornerRadius: 0.0,
|
||||
padding: BlockStyle.Padding(horizontal: 25, vertical: 12),
|
||||
type: .side(offset: 5, width: 4)))
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ struct RoomScreen: View {
|
||||
@State private var dragOver = false
|
||||
let composerToolbar: ComposerToolbar
|
||||
|
||||
private let attachmentButtonPadding = 10.0
|
||||
|
||||
init(context: RoomScreenViewModel.Context, composerToolbar: ComposerToolbar) {
|
||||
self.context = context
|
||||
self.composerToolbar = composerToolbar
|
||||
@@ -36,13 +34,13 @@ struct RoomScreen: View {
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
composerToolbar
|
||||
.padding(.leading, attachmentButtonPadding)
|
||||
.padding(.trailing, 12)
|
||||
.padding(.bottom)
|
||||
.padding(.leading, 5)
|
||||
.padding(.trailing, 8)
|
||||
.padding(.bottom, composerToolbarContext.composerActionsEnabled ? 8 : 12)
|
||||
.background {
|
||||
if composerToolbarContext.composerActionsEnabled {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.stroke(Color.compound._borderTextFieldFocused, lineWidth: 1)
|
||||
.stroke(Color.compound.borderInteractiveSecondary, lineWidth: 0.5)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user