PTT fixes, TDS expansions, performance hooks, state event renderers

PTT fixes (BUG-7/8/9):
- BUG-7: Fix isEditable to use ownerDocument.body (works in EC iframe context)
- BUG-8: Release mic if callEmbed changes during active PTT (cleanup fn)
- BUG-9: Wire iframe blur/focus listeners for stuck-mic prevention

PiP bug fix (BUG-11):
- Track prevPipMode ref so position only resets when first entering pip mode,
  not on every callVisible change (user drag position preserved)

GIF improvements (BUG-15):
- Show user-visible error text in toolbar on GIF send failure
- Also surface size-limit rejection with a 4-second auto-clearing message

Performance:
- useInterval: replace useMemo with useEffect for setInterval (React StrictMode safe)
- usePan: add unmount cleanup effect to remove document mousemove/mouseup listeners

Feature: timeline state event renderers (low effort, high value):
- Added renderers for RoomEncryption, RoomJoinRules, RoomGuestAccess, RoomCanonicalAlias
- These were silently falling through to the hidden-events fallback

TDS (Lotus Terminal Design System):
- Fix: correct 8 escaped template literals in GIF picker light-mode block
- Add data-emoji-board attribute to EmojiBoardLayout for stable CSS targeting
- Add Tooltip panel styles (dark + light mode)
- Add Switch toggle styles (dark + light mode)
- Add Spinner stroke colors (dark + light mode)
- Add EmojiBoard panel styles (dark + light mode)
- Add PopOut/Menu/floating panel styles (dark + light mode)
This commit is contained in:
Lotus Bot
2026-05-19 16:45:02 -04:00
parent 63e1085984
commit 2b2619145c
8 changed files with 241 additions and 38 deletions
+13 -1
View File
@@ -241,6 +241,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const { gifApiKey } = useClientConfig();
const gifBtnRef = useRef<HTMLButtonElement>(null);
const [hideStickerBtn, setHideStickerBtn] = useState(document.body.clientWidth < 500);
const [gifError, setGifError] = React.useState<string | null>(null);
const isComposing = useComposingCheck();
@@ -469,7 +470,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
if (!contentType.startsWith('image/')) return;
const blob = await res.blob();
if (blob.size > 20 * 1024 * 1024) return; // 20 MB cap
if (blob.size > 20 * 1024 * 1024) {
setGifError('GIF is too large (max 20 MB).');
setTimeout(() => setGifError(null), 4000);
return;
}
const uploadRes = await mx.uploadContent(
new File([blob], 'image.gif', { type: 'image/gif' }),
@@ -485,6 +490,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
});
} catch (e) {
console.error('GIF send failed', e);
setGifError('Failed to send GIF. Please try again.');
setTimeout(() => setGifError(null), 4000);
}
},
[mx, roomId]
@@ -774,6 +781,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
)}
</UseStateProvider>
)}
{gifError && (
<Text size="T100" style={{ color: 'var(--tc-danger-normal)', padding: '2px 6px', alignSelf: 'center', whiteSpace: 'nowrap' }}>
{gifError}
</Text>
)}
<IconButton
onClick={handleShareLocation}
variant="SurfaceVariant"
+64
View File
@@ -1586,6 +1586,70 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
</Event>
);
},
[StateEvent.RoomEncryption]: (mEventId, mEvent, item) => {
const highlighted = focusItem?.index === item && focusItem.highlight;
const senderId = mEvent.getSender() ?? '';
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
);
return (
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Lock}
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{' enabled end-to-end encryption'}</Text></Box>}
/>
</Event>
);
},
[StateEvent.RoomJoinRules]: (mEventId, mEvent, item) => {
const highlighted = focusItem?.index === item && focusItem.highlight;
const senderId = mEvent.getSender() ?? '';
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const joinRule = mEvent.getContent<{ join_rule?: string }>().join_rule ?? 'unknown';
const ruleLabel: Record<string, string> = { public: 'public', invite: 'invite-only', knock: 'knock', restricted: 'restricted' };
const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
);
return (
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Settings}
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{` set room join rule to ${ruleLabel[joinRule] ?? joinRule}`}</Text></Box>}
/>
</Event>
);
},
[StateEvent.RoomGuestAccess]: (mEventId, mEvent, item) => {
const highlighted = focusItem?.index === item && focusItem.highlight;
const senderId = mEvent.getSender() ?? '';
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const access = mEvent.getContent<{ guest_access?: string }>().guest_access ?? 'unknown';
const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
);
return (
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Settings}
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{access === 'can_join' ? ' allowed guest access' : ' disabled guest access'}</Text></Box>}
/>
</Event>
);
},
[StateEvent.RoomCanonicalAlias]: (mEventId, mEvent, item) => {
const highlighted = focusItem?.index === item && focusItem.highlight;
const senderId = mEvent.getSender() ?? '';
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const alias = mEvent.getContent<{ alias?: string }>().alias;
const timeJSX = (
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
);
return (
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Hash}
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{alias ? ` set room address to ${alias}` : ' removed room address'}</Text></Box>}
/>
</Event>
);
},
[StateEvent.GroupCallMemberPrefix]: (mEventId, mEvent, item) => {
const highlighted = focusItem?.index === item && focusItem.highlight;
const senderId = mEvent.getSender() ?? '';