mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Support adding a caption to media uploads. (#3531)
* Add a composer to the MediaUploadPreviewScreen. And send it's content to the media upload's caption. * Use the new compound SendButton (updating relative padding in the toolbar). * Update snapshots. * Add unit tests for sending a caption.
This commit is contained in:
@@ -62,7 +62,7 @@ struct ComposerToolbar: View {
|
||||
if !context.composerFormattingEnabled {
|
||||
if context.viewState.isUploading {
|
||||
ProgressView()
|
||||
.scaledFrame(size: 44, relativeTo: .title)
|
||||
.scaledFrame(size: 44, relativeTo: .compound.headingLG)
|
||||
.padding(.leading, 3)
|
||||
} else if context.viewState.showSendButton {
|
||||
sendButton
|
||||
@@ -119,27 +119,29 @@ struct ComposerToolbar: View {
|
||||
Image(Asset.Images.closeRte.name)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.scaledFrame(size: 30, relativeTo: .title)
|
||||
.scaledPadding(7, relativeTo: .title)
|
||||
.scaledFrame(size: 30, relativeTo: .compound.headingLG)
|
||||
.scaledPadding(7, relativeTo: .compound.headingLG)
|
||||
}
|
||||
.accessibilityLabel(L10n.actionClose)
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions)
|
||||
}
|
||||
|
||||
private var sendButton: some View {
|
||||
Button {
|
||||
sendMessage()
|
||||
} label: {
|
||||
CompoundIcon(context.viewState.composerMode.isEdit ? \.check : \.sendSolid)
|
||||
.scaledPadding(6, relativeTo: .title)
|
||||
.accessibilityLabel(context.viewState.composerMode.isEdit ? L10n.actionConfirm : L10n.actionSend)
|
||||
.foregroundColor(context.viewState.sendButtonDisabled ? .compound.iconDisabled : .white)
|
||||
.background {
|
||||
Circle()
|
||||
.foregroundColor(context.viewState.sendButtonDisabled ? .clear : .compound.iconAccentTertiary)
|
||||
Group {
|
||||
if context.viewState.composerMode.isEdit {
|
||||
Button(action: sendMessage) {
|
||||
CompoundIcon(\.check, size: .medium, relativeTo: .compound.headingLG)
|
||||
.foregroundColor(.white)
|
||||
.scaledPadding(6, relativeTo: .compound.headingLG)
|
||||
.background(.compound.iconAccentTertiary, in: Circle())
|
||||
.accessibilityLabel(L10n.actionConfirm)
|
||||
}
|
||||
.scaledPadding(4, relativeTo: .title)
|
||||
} else {
|
||||
SendButton(action: sendMessage)
|
||||
.accessibilityLabel(L10n.actionSend)
|
||||
}
|
||||
}
|
||||
.scaledPadding(4, relativeTo: .compound.headingLG)
|
||||
.disabled(context.viewState.sendButtonDisabled)
|
||||
.animation(.linear(duration: 0.1).disabledDuringTests(), value: context.viewState.sendButtonDisabled)
|
||||
.keyboardShortcut(.return, modifiers: [.command])
|
||||
@@ -271,8 +273,8 @@ struct ComposerToolbar: View {
|
||||
} label: {
|
||||
CompoundIcon(\.delete)
|
||||
.scaledToFit()
|
||||
.scaledFrame(size: 30, relativeTo: .title)
|
||||
.scaledPadding(7, relativeTo: .title)
|
||||
.scaledFrame(size: 30, relativeTo: .compound.headingLG)
|
||||
.scaledPadding(7, relativeTo: .compound.headingLG)
|
||||
}
|
||||
.buttonStyle(.compound(.plain))
|
||||
.accessibilityLabel(L10n.a11yDelete)
|
||||
@@ -291,6 +293,8 @@ struct ComposerToolbar: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
|
||||
static let wysiwygViewModel = WysiwygComposerViewModel()
|
||||
static let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
|
||||
@@ -330,8 +334,6 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mock
|
||||
|
||||
extension ComposerToolbar {
|
||||
static func mock(focused: Bool = true) -> ComposerToolbar {
|
||||
let wysiwygViewModel = WysiwygComposerViewModel()
|
||||
|
||||
@@ -35,17 +35,8 @@ struct MessageComposer: View {
|
||||
resizeGrabber
|
||||
}
|
||||
|
||||
mainContent
|
||||
.padding(.horizontal, 12.0)
|
||||
.clipShape(composerShape)
|
||||
.background {
|
||||
ZStack {
|
||||
composerShape
|
||||
.fill(Color.compound.bgSubtleSecondary)
|
||||
composerShape
|
||||
.stroke(Color.compound._borderTextFieldFocused, lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
composerTextField
|
||||
.messageComposerStyle(header: header)
|
||||
// Explicitly disable all animations to fix weirdness with the header immediately
|
||||
// appearing whilst the text field and keyboard are still animating up to it.
|
||||
.animation(.noAnimation, value: mode)
|
||||
@@ -57,34 +48,27 @@ struct MessageComposer: View {
|
||||
|
||||
@State private var composerFrame = CGRect.zero
|
||||
|
||||
private var mainContent: some View {
|
||||
VStack(alignment: .leading, spacing: -6) {
|
||||
header
|
||||
|
||||
if composerFormattingEnabled {
|
||||
Color.clear
|
||||
.overlay(alignment: .top) {
|
||||
composerView
|
||||
.clipped()
|
||||
.readFrame($composerFrame)
|
||||
}
|
||||
.frame(minHeight: ComposerConstant.minHeight, maxHeight: max(composerHeight, composerFrame.height),
|
||||
alignment: .top)
|
||||
.tint(.compound.iconAccentTertiary)
|
||||
.padding(.vertical, 10)
|
||||
.onAppear {
|
||||
onAppearAction()
|
||||
}
|
||||
} else {
|
||||
MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder,
|
||||
text: $plainComposerText,
|
||||
presendCallback: $presendCallback,
|
||||
maxHeight: ComposerConstant.maxHeight,
|
||||
keyHandler: { handleKeyPress($0) },
|
||||
pasteHandler: pasteAction)
|
||||
.tint(.compound.iconAccentTertiary)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
@ViewBuilder
|
||||
private var composerTextField: some View {
|
||||
if composerFormattingEnabled {
|
||||
Color.clear
|
||||
.overlay(alignment: .top) {
|
||||
composerView
|
||||
.clipped()
|
||||
.readFrame($composerFrame)
|
||||
}
|
||||
.frame(minHeight: ComposerConstant.minHeight, maxHeight: max(composerHeight, composerFrame.height),
|
||||
alignment: .top)
|
||||
.onAppear {
|
||||
onAppearAction()
|
||||
}
|
||||
} else {
|
||||
MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder,
|
||||
text: $plainComposerText,
|
||||
presendCallback: $presendCallback,
|
||||
maxHeight: ComposerConstant.maxHeight,
|
||||
keyHandler: { handleKeyPress($0) },
|
||||
pasteHandler: pasteAction)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +184,42 @@ private struct MessageComposerHeaderLabelStyle: LabelStyle {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Style
|
||||
|
||||
extension View {
|
||||
func messageComposerStyle(header: some View = EmptyView()) -> some View {
|
||||
modifier(MessageComposerStyleModifier(header: header))
|
||||
}
|
||||
}
|
||||
|
||||
private struct MessageComposerStyleModifier<Header: View>: ViewModifier {
|
||||
private let composerShape = RoundedRectangle(cornerRadius: 21, style: .circular)
|
||||
|
||||
let header: Header
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
VStack(alignment: .leading, spacing: -6) {
|
||||
header
|
||||
|
||||
content
|
||||
.tint(.compound.iconAccentTertiary)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
.padding(.horizontal, 12.0)
|
||||
.clipShape(composerShape)
|
||||
.background {
|
||||
ZStack {
|
||||
composerShape
|
||||
.fill(Color.compound.bgSubtleSecondary)
|
||||
composerShape
|
||||
.stroke(Color.compound.borderInteractiveSecondary, lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct MessageComposer_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ struct RoomAttachmentPicker: View {
|
||||
Menu {
|
||||
menuContent
|
||||
} label: {
|
||||
CompoundIcon(asset: Asset.Images.composerAttachment, size: .custom(30), relativeTo: .title)
|
||||
.scaledPadding(7, relativeTo: .title)
|
||||
CompoundIcon(asset: Asset.Images.composerAttachment, size: .custom(30), relativeTo: .compound.headingLG)
|
||||
.scaledPadding(7, relativeTo: .compound.headingLG)
|
||||
}
|
||||
.buttonStyle(RoomAttachmentPickerButtonStyle())
|
||||
.accessibilityLabel(L10n.actionAddToTimeline)
|
||||
|
||||
+6
-9
@@ -32,18 +32,15 @@ struct VoiceMessageRecordingButton: View {
|
||||
} label: {
|
||||
switch mode {
|
||||
case .idle:
|
||||
CompoundIcon(\.micOn, size: .medium, relativeTo: .title)
|
||||
CompoundIcon(\.micOn, size: .medium, relativeTo: .compound.headingLG)
|
||||
.foregroundColor(.compound.iconSecondary)
|
||||
.scaledPadding(10, relativeTo: .title)
|
||||
.scaledPadding(10, relativeTo: .compound.headingLG)
|
||||
case .recording:
|
||||
CompoundIcon(asset: Asset.Images.stopRecording, size: .medium, relativeTo: .title)
|
||||
CompoundIcon(asset: Asset.Images.stopRecording, size: .medium, relativeTo: .compound.headingLG)
|
||||
.foregroundColor(.compound.iconOnSolidPrimary)
|
||||
.scaledPadding(6, relativeTo: .title)
|
||||
.background(
|
||||
Circle()
|
||||
.foregroundColor(.compound.bgActionPrimaryRest)
|
||||
)
|
||||
.scaledPadding(4, relativeTo: .title)
|
||||
.scaledPadding(6, relativeTo: .compound.headingLG)
|
||||
.background(.compound.bgActionPrimaryRest, in: Circle())
|
||||
.scaledPadding(4, relativeTo: .compound.headingLG)
|
||||
}
|
||||
}
|
||||
.buttonStyle(VoiceMessageRecordingButtonStyle())
|
||||
|
||||
Reference in New Issue
Block a user