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:
Doug
2024-11-19 16:35:01 +00:00
committed by GitHub
parent 352bb577ad
commit b75ad6a5aa
56 changed files with 530 additions and 284 deletions
@@ -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)
@@ -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())