ui: forward dialog avatars, poll TDS, delivery icon, caption focus, boot hint
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 <noreply@anthropic.com>
This commit is contained in:
@@ -216,6 +216,7 @@ export function PollContent({
|
||||
|
||||
return (
|
||||
<Box
|
||||
data-poll-content
|
||||
direction="Column"
|
||||
gap="200"
|
||||
style={{ maxWidth: '340px', paddingTop: '2px', paddingBottom: '4px' }}
|
||||
@@ -223,6 +224,7 @@ export function PollContent({
|
||||
<Box
|
||||
alignItems="Center"
|
||||
gap="100"
|
||||
data-poll-content-label
|
||||
style={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 700,
|
||||
@@ -251,6 +253,8 @@ export function PollContent({
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
data-poll-answer
|
||||
data-selected={selected}
|
||||
onClick={canVote ? () => handleVote(id) : undefined}
|
||||
style={{
|
||||
padding: '7px 12px',
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<MenuItem
|
||||
size="300"
|
||||
radii="300"
|
||||
onClick={onClick}
|
||||
disabled={sending}
|
||||
before={
|
||||
<Avatar size="200" radii="300">
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={avatarUrl}
|
||||
alt={room.name}
|
||||
renderFallback={() => (
|
||||
<RoomIcon
|
||||
roomType={room.getType()}
|
||||
size="100"
|
||||
joinRule={room.getJoinRule()}
|
||||
filled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
<Box direction="Column">
|
||||
<Text size="T300" truncate>
|
||||
{room.name}
|
||||
</Text>
|
||||
{dm && (
|
||||
<Text size="T200" priority="300" truncate>
|
||||
Direct Message
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
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<string | null>(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<string, unknown> = { ...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<string, unknown> = { ...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 (
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
@@ -59,7 +130,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
<Modal
|
||||
size="400"
|
||||
style={{
|
||||
maxHeight: '440px',
|
||||
maxHeight: '480px',
|
||||
borderRadius: config.radii.R500,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -72,15 +143,17 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
style={{ padding: config.space.S400, paddingBottom: config.space.S200 }}
|
||||
>
|
||||
<Text size="H5">Forward message</Text>
|
||||
<Input
|
||||
variant="Background"
|
||||
size="400"
|
||||
radii="400"
|
||||
outlined
|
||||
placeholder="Search rooms…"
|
||||
value={query}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
|
||||
/>
|
||||
{!sentTo && (
|
||||
<Input
|
||||
variant="Background"
|
||||
size="400"
|
||||
radii="400"
|
||||
outlined
|
||||
placeholder="Search rooms…"
|
||||
value={query}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Line size="300" />
|
||||
{sentTo ? (
|
||||
@@ -88,6 +161,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
grow="Yes"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
gap="300"
|
||||
style={{ padding: config.space.S400 }}
|
||||
>
|
||||
<Text size="T300">✓ Forwarded to {sentTo}</Text>
|
||||
@@ -97,16 +171,14 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
<Scroll size="300" hideTrack visibility="Hover">
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{filtered.slice(0, 60).map((room) => (
|
||||
<MenuItem
|
||||
<RoomRow
|
||||
key={room.roomId}
|
||||
size="300"
|
||||
radii="300"
|
||||
onClick={() => forward(room.roomId, room.name)}
|
||||
>
|
||||
<Text size="T300" truncate>
|
||||
{room.name}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
room={room}
|
||||
dm={directs.has(room.roomId)}
|
||||
useAuthentication={useAuthentication}
|
||||
onClick={() => forward(room)}
|
||||
sending={sending}
|
||||
/>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<Box
|
||||
@@ -121,6 +193,20 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
)}
|
||||
</Box>
|
||||
</Scroll>
|
||||
{sending && (
|
||||
<Box
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'rgba(0,0,0,0.18)',
|
||||
borderRadius: config.radii.R500,
|
||||
}}
|
||||
>
|
||||
<Spinner variant="Secondary" size="400" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user