diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 3fb66aa..ee4bf8a 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -70,7 +70,13 @@ send. Use **Shift+Enter** to add a line break without sending. ## Formatting -Messages support Markdown: +Messages support Markdown. Prefer buttons to typing the syntax? Click the +**Aa** button next to the emoji button in the composer to open a formatting +toolbar — it inserts the markdown below into your message for you (select some +text first to wrap it, or click a button again to take the formatting back +off). It stays collapsed until you open it, and remembers your choice. + +Or type it yourself: - `**bold**`, `*italic*`, `~~strikethrough~~` - `` `inline code` `` and fenced code blocks (three backticks) diff --git a/frontend/src/components/Composer.css b/frontend/src/components/Composer.css index 1683f75..4736a26 100644 --- a/frontend/src/components/Composer.css +++ b/frontend/src/components/Composer.css @@ -105,6 +105,34 @@ cursor: not-allowed; } +.composer-format-toggle { + width: 36px; + height: 36px; + flex: none; + border-radius: var(--radius); + background: transparent; + color: var(--ds-muted); + border: 1px solid var(--ds-border); + font-size: 0.82rem; + font-weight: 700; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.composer-format-toggle:hover:not(:disabled), +.composer-format-toggle-open { + color: var(--ds-text); + border-color: var(--ds-accent); +} + +.composer-format-toggle:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .composer-attach-wrap { position: relative; flex: none; diff --git a/frontend/src/components/Composer.tsx b/frontend/src/components/Composer.tsx index bbfbdf4..65e1cf0 100644 --- a/frontend/src/components/Composer.tsx +++ b/frontend/src/components/Composer.tsx @@ -15,10 +15,13 @@ import { getUploadLimit } from '../api/uploads' import { useCustomEmoji } from '../context/CustomEmojiContext' import { EMOJI_SHORTCODES, SHORTCODE_BY_GLYPH } from '../lib/emojiShortcodes' import { formatFileSize } from '../lib/fileSize' +import { getFormattingToolbarOpen, setFormattingToolbarOpen } from '../lib/formattingToolbarPref' +import { applyFormat, type FormatAction } from '../lib/markdownFormat' import { getRecentEmoji, recordEmojiUsed } from '../lib/recentEmoji' import type { MyRoomItem, RoomMember } from '../types' import { EmojiPicker } from './EmojiPicker' import { EmojiShortcodeAutocomplete, type EmojiShortcodeMatch } from './EmojiShortcodeAutocomplete' +import { FormattingToolbar } from './FormattingToolbar' import { MentionAutocomplete } from './MentionAutocomplete' import { RoomReferenceAutocomplete } from './RoomReferenceAutocomplete' import './Composer.css' @@ -136,6 +139,7 @@ export function Composer({ roomId, roomName, isDm, members, rooms, disabled, arc const [emojiActiveIndex, setEmojiActiveIndex] = useState(0) const [dragActive, setDragActive] = useState(false) const [attachMenuOpen, setAttachMenuOpen] = useState(false) + const [toolbarOpen, setToolbarOpen] = useState(getFormattingToolbarOpen) const { byShortcode: customEmojiByShortcode } = useCustomEmoji() const textareaRef = useRef(null) const fileInputRef = useRef(null) @@ -449,6 +453,29 @@ export function Composer({ roomId, roomName, isDm, members, rooms, disabled, arc }) } + function toggleToolbar() { + const next = !toolbarOpen + setToolbarOpen(next) + setFormattingToolbarOpen(next) + } + + // #75: the toolbar only ever edits the textarea's plain markdown text -- + // nothing about what gets sent changes. + function applyFormatAction(action: FormatAction) { + const el = textareaRef.current + if (!el) return + const result = applyFormat( + { value, start: el.selectionStart ?? value.length, end: el.selectionEnd ?? value.length }, + action, + ) + setValue(result.value) + requestAnimationFrame(() => { + el.focus() + el.setSelectionRange(result.start, result.end) + autoGrow() + }) + } + function insertEmoji(emoji: string) { const el = textareaRef.current setEmojiPickerOpen(false) @@ -517,6 +544,9 @@ export function Composer({ roomId, roomName, isDm, members, rooms, disabled, arc )} {uploadError &&
{uploadError}
} + {toolbarOpen && ( + + )}
)}
+