From df99038ad68d281fff8243e0ba729fc4aa54ba25 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 23 May 2026 23:01:13 -0400 Subject: [PATCH] ui: forward dialog avatars, poll TDS, delivery icon, caption focus, boot hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ForwardMessageDialog: - Room list now shows small avatars (48px crop) + DM label beneath room name - Forward is now async: spinner overlay while in-flight, '✓ Forwarded' only shown after sendEvent resolves; error clears sending state so user can retry - Search bar hidden in success state for cleaner confirmation view DeliveryStatus: - QUEUED state used ⏳ emoji breaking the ASCII/terminal aesthetic; changed to ⟳ matching the SENDING/ENCRYPTING icon PollContent: - Added data-poll-content + data-poll-answer + data-selected attributes so TDS CSS can override inline styles without JS branching - Added data-poll-content-label on the ◉ Poll header - TDS dark: answers get cyan dim bg/border, selected gets orange highlight with subtle box-shadow; hover brightens border; label uses cyan glow - TDS light: equivalent blue/orange variants Caption input: - Marked with data-caption-input; focus-visible ring added in index.css (blue for default, dark-theme dark blue) and lotus-terminal.css.ts (orange glow for TDS dark, orange for TDS light) Boot sequence: - Added '[ ESC ] skip' hint at bottom-right of overlay so users know they can dismiss it without waiting Co-Authored-By: Claude Sonnet 4.6 --- .../message/content/PollContent.tsx | 4 + .../upload-card/UploadCardRenderer.tsx | 2 + .../room/message/ForwardMessageDialog.tsx | 144 ++++++++++++++---- src/app/features/room/message/Message.tsx | 2 +- src/index.css | 11 ++ src/lotus-boot.ts | 17 +++ src/lotus-terminal.css.ts | 52 +++++++ 7 files changed, 202 insertions(+), 30 deletions(-) diff --git a/src/app/components/message/content/PollContent.tsx b/src/app/components/message/content/PollContent.tsx index 721c74dcb..86b8152b8 100644 --- a/src/app/components/message/content/PollContent.tsx +++ b/src/app/components/message/content/PollContent.tsx @@ -216,6 +216,7 @@ export function PollContent({ return ( handleVote(id) : undefined} style={{ padding: '7px 12px', diff --git a/src/app/components/upload-card/UploadCardRenderer.tsx b/src/app/components/upload-card/UploadCardRenderer.tsx index 4e251cc75..98ef56dec 100644 --- a/src/app/components/upload-card/UploadCardRenderer.tsx +++ b/src/app/components/upload-card/UploadCardRenderer.tsx @@ -188,6 +188,7 @@ export function UploadCardRenderer({ placeholder="Add a caption… (optional)" value={metadata.caption ?? ''} onChange={(e) => setMetadata(fileItem, { ...metadata, caption: e.target.value })} + data-caption-input style={{ marginTop: '6px', width: '100%', @@ -199,6 +200,7 @@ export function UploadCardRenderer({ color: 'var(--text-primary)', outline: 'none', boxSizing: 'border-box', + transition: 'border-color 0.15s, box-shadow 0.15s', }} /> )} diff --git a/src/app/features/room/message/ForwardMessageDialog.tsx b/src/app/features/room/message/ForwardMessageDialog.tsx index 8decc44c5..7e656710a 100644 --- a/src/app/features/room/message/ForwardMessageDialog.tsx +++ b/src/app/features/room/message/ForwardMessageDialog.tsx @@ -1,6 +1,7 @@ -import React, { ChangeEvent, useState } from 'react'; +import React, { ChangeEvent, useCallback, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { + Avatar, Box, config, Input, @@ -11,11 +12,69 @@ import { OverlayBackdrop, OverlayCenter, Scroll, + Spinner, Text, } from 'folds'; -import { MatrixEvent } from 'matrix-js-sdk'; +import { MatrixEvent, Room } from 'matrix-js-sdk'; +import { useAtomValue } from 'jotai'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { stopPropagation } from '../../../utils/keyboard'; +import { mDirectAtom } from '../../../state/mDirectList'; +import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; +import { mxcUrlToHttp } from '../../../utils/matrix'; +import { RoomAvatar, RoomIcon } from '../../../components/room-avatar'; + +type RoomRowProps = { + room: Room; + dm: boolean; + useAuthentication: boolean; + onClick: () => void; + sending: boolean; +}; +function RoomRow({ room, dm, useAuthentication, onClick, sending }: RoomRowProps) { + const mx = useMatrixClient(); + const avatarMxc = room.getMxcAvatarUrl(); + const avatarUrl = avatarMxc + ? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined) + : undefined; + + return ( + + ( + + )} + /> + + } + > + + + {room.name} + + {dm && ( + + Direct Message + + )} + + + ); +} type Props = { mEvent: MatrixEvent; @@ -24,7 +83,10 @@ type Props = { export function ForwardMessageDialog({ mEvent, onClose }: Props) { const mx = useMatrixClient(); + const directs = useAtomValue(mDirectAtom); + const useAuthentication = useMediaAuthentication(); const [query, setQuery] = useState(''); + const [sending, setSending] = useState(false); const [sentTo, setSentTo] = useState(null); const allRooms = mx @@ -36,14 +98,23 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) { ? allRooms.filter((r) => r.name.toLowerCase().includes(query.toLowerCase())) : allRooms; - const forward = (roomId: string, roomName: string) => { - const fwdContent: Record = { ...mEvent.getContent() }; - delete fwdContent['m.relates_to']; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (mx as any).sendEvent(roomId, mEvent.getType(), fwdContent); - setSentTo(roomName); - setTimeout(onClose, 1200); - }; + const forward = useCallback( + async (room: Room) => { + if (sending) return; + setSending(true); + const fwdContent: Record = { ...mEvent.getContent() }; + delete fwdContent['m.relates_to']; + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (mx as any).sendEvent(room.roomId, mEvent.getType(), fwdContent); + setSentTo(room.name); + setTimeout(onClose, 1400); + } catch { + setSending(false); + } + }, + [mx, mEvent, onClose, sending], + ); return ( }> @@ -59,7 +130,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) { Forward message - ) => setQuery(e.target.value)} - /> + {!sentTo && ( + ) => setQuery(e.target.value)} + /> + )} {sentTo ? ( @@ -88,6 +161,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) { grow="Yes" alignItems="Center" justifyContent="Center" + gap="300" style={{ padding: config.space.S400 }} > ✓ Forwarded to {sentTo} @@ -97,16 +171,14 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) { {filtered.slice(0, 60).map((room) => ( - forward(room.roomId, room.name)} - > - - {room.name} - - + room={room} + dm={directs.has(room.roomId)} + useAuthentication={useAuthentication} + onClick={() => forward(room)} + sending={sending} + /> ))} {filtered.length === 0 && ( + {sending && ( + + + + )} )} diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 95db1db20..325498b0f 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -96,7 +96,7 @@ function DeliveryStatus({ label = 'Failed to send'; colorStyle = lotusTerminal ? '#FF3B3B' : color.Critical.Main; } else if (status === EventStatus.QUEUED) { - icon = '⏳'; + icon = '⟳'; label = 'Queued'; colorStyle = lotusTerminal ? 'rgba(0,212,255,0.45)' : color.Secondary.Main; } else if (status === EventStatus.SENDING || status === EventStatus.ENCRYPTING) { diff --git a/src/index.css b/src/index.css index 89b44f309..89a7f0206 100644 --- a/src/index.css +++ b/src/index.css @@ -141,6 +141,17 @@ textarea { word-spacing: inherit; } +/* Caption input focus ring — replaces stripped outline */ +[data-caption-input]:focus-visible { + border-color: rgba(0, 100, 200, 0.55); + box-shadow: 0 0 0 2px rgba(0, 100, 200, 0.18); +} +.dark-theme [data-caption-input]:focus-visible, +.butter-theme [data-caption-input]:focus-visible { + border-color: rgba(100, 160, 255, 0.60); + box-shadow: 0 0 0 2px rgba(100, 160, 255, 0.18); +} + audio:not([controls]) { display: none !important; } diff --git a/src/lotus-boot.ts b/src/lotus-boot.ts index 65c22a716..8187b0ad3 100644 --- a/src/lotus-boot.ts +++ b/src/lotus-boot.ts @@ -55,8 +55,25 @@ export function runLotusBootSequence(force = false): void { 'line-height:1.7', 'white-space:pre-wrap', 'overflow:hidden', + 'flex:1', ].join(';'); overlay.appendChild(pre); + + const escHint = document.createElement('div'); + escHint.style.cssText = [ + 'position:absolute', + 'bottom:1.5rem', + 'right:2rem', + 'font-size:0.68rem', + 'color:rgba(0,255,136,0.35)', + "font-family:'JetBrains Mono','Courier New',monospace", + 'letter-spacing:0.08em', + 'user-select:none', + 'pointer-events:none', + ].join(';'); + escHint.textContent = '[ ESC ] skip'; + overlay.appendChild(escHint); + document.body.appendChild(overlay); const dismiss = (): void => { diff --git a/src/lotus-terminal.css.ts b/src/lotus-terminal.css.ts index 1bd9f3934..651953b63 100644 --- a/src/lotus-terminal.css.ts +++ b/src/lotus-terminal.css.ts @@ -878,3 +878,55 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} ._13tt0gb6: background: 'rgba(0,98,184,0.08) !important' as any, color: '#0062b8 !important' as any, }); + +// ── Poll card TDS ───────────────────────────────────────────────────────────── +globalStyle(`body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-answer]`, { + background: 'rgba(0,212,255,0.04) !important' as any, + border: '1px solid rgba(0,212,255,0.22) !important' as any, + color: '#c4d9ee !important' as any, + transition: 'border-color 0.15s, background 0.15s, box-shadow 0.15s', +}); +globalStyle(`body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-answer]:hover`, { + background: 'rgba(0,212,255,0.08) !important' as any, + borderColor: 'rgba(0,212,255,0.40) !important' as any, + boxShadow: '0 0 8px rgba(0,212,255,0.08)', +}); +globalStyle( + `body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-answer][data-selected="true"]`, + { + background: 'rgba(255,107,0,0.10) !important' as any, + border: '1px solid rgba(255,107,0,0.55) !important' as any, + boxShadow: '0 0 10px rgba(255,107,0,0.10)', + }, +); +globalStyle(`body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-content-label]`, { + color: 'rgba(0,212,255,0.60) !important' as any, + opacity: '1 !important' as any, + textShadow: '0 0 8px rgba(0,212,255,0.25)', +}); +// light TDS +globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-answer]`, { + background: 'rgba(0,98,184,0.04) !important' as any, + border: '1px solid rgba(0,98,184,0.22) !important' as any, + color: '#111827 !important' as any, +}); +globalStyle( + `html[data-theme="light"] body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-answer][data-selected="true"]`, + { + background: 'rgba(196,78,0,0.08) !important' as any, + border: '1px solid rgba(196,78,0,0.50) !important' as any, + }, +); + +// ── Caption input TDS focus ring ────────────────────────────────────────────── +globalStyle(`body.${lotusTerminalBodyClass} [data-caption-input]:focus-visible`, { + borderColor: 'rgba(255,107,0,0.65) !important' as any, + boxShadow: '0 0 0 2px rgba(255,107,0,0.14), 0 0 6px rgba(255,107,0,0.10) !important' as any, +}); +globalStyle( + `html[data-theme="light"] body.${lotusTerminalBodyClass} [data-caption-input]:focus-visible`, + { + borderColor: 'rgba(196,78,0,0.60) !important' as any, + boxShadow: '0 0 0 2px rgba(196,78,0,0.12) !important' as any, + }, +);