CI / Build & Quality Checks (pull_request) Successful in 3m36s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 18s
CI / Playwright smoke (e2e) (pull_request) Successful in 16m40s
MSC2931: a widget with m.always_on_screen (auto-granted) can send set_always_on_screen; the host now answers it, and a widget that asked keeps running in a small floating window (title, Back to room, close) when the Widgets panel closes or you switch rooms. Back to room returns to it with the panel open on that widget, without reloading it. Moving an iframe reloads it, so widget frames now live in an app-level layer from the start (widgetSessions.ts) and are positioned over the panel's placeholder while docked. A widget that did not ask is torn down as before. One floating widget at a time; it also goes when the widget withdraws, is removed from the room, or on logout. A permission request while floating grants nothing extra. Refs #205 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
import React, { useEffect, useSyncExternalStore } from 'react';
|
|
import { Box, color, config, Icon, IconButton, Icons, Text } from 'folds';
|
|
import { useSetAtom } from 'jotai';
|
|
import {
|
|
destroyAllWidgetSessions,
|
|
destroyWidgetSession,
|
|
FLOATING_WIDGET,
|
|
floatingWidgetSessions,
|
|
subscribeWidgetSessions,
|
|
widgetSessionsVersion,
|
|
} from './widgetSessions';
|
|
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
|
import { widgetViewingAtom, widgetsPanelAtom } from '../../../state/widgetsPanel';
|
|
import { zIndices } from '../../../styles/zIndex';
|
|
|
|
/**
|
|
* [Gitea #205] Title bar for a widget kept on screen (MSC2931) after its room
|
|
* or the Widgets panel was left. The widget's own frame is positioned by
|
|
* widgetSessions.ts right below this bar. Mounted once for the signed-in
|
|
* client; unmounting (logout) stops every widget.
|
|
*/
|
|
export function FloatingWidget() {
|
|
useSyncExternalStore(subscribeWidgetSessions, widgetSessionsVersion);
|
|
const { navigateRoom } = useRoomNavigate();
|
|
const setPanel = useSetAtom(widgetsPanelAtom);
|
|
const setViewing = useSetAtom(widgetViewingAtom);
|
|
useEffect(() => () => destroyAllWidgetSessions(), []);
|
|
|
|
const session = floatingWidgetSessions()[0];
|
|
if (!session) return null;
|
|
const f = FLOATING_WIDGET;
|
|
const back = () => {
|
|
setViewing((prev) => ({ ...prev, [session.roomId]: session.widgetId }));
|
|
setPanel(true);
|
|
navigateRoom(session.roomId);
|
|
};
|
|
|
|
return (
|
|
<Box
|
|
role="region"
|
|
aria-label={`${session.name} (kept on screen)`}
|
|
alignItems="Center"
|
|
gap="100"
|
|
style={{
|
|
position: 'fixed',
|
|
right: f.right,
|
|
bottom: f.bottom + f.height,
|
|
width: f.width,
|
|
height: f.bar,
|
|
zIndex: zIndices.floatingWidget,
|
|
padding: `0 ${config.space.S100} 0 ${config.space.S300}`,
|
|
background: color.Surface.Container,
|
|
color: color.Surface.OnContainer,
|
|
borderRadius: '8px 8px 0 0',
|
|
boxShadow: '0 8px 24px rgba(0,0,0,0.35)',
|
|
}}
|
|
>
|
|
<Icon size="100" src={Icons.Category} />
|
|
<Text size="T300" truncate style={{ flexGrow: 1 }}>
|
|
{session.name}
|
|
</Text>
|
|
<IconButton size="300" radii="300" variant="Surface" aria-label="Back to room" onClick={back}>
|
|
<Icon size="100" src={Icons.ArrowGoLeft} />
|
|
</IconButton>
|
|
<IconButton
|
|
size="300"
|
|
radii="300"
|
|
variant="Surface"
|
|
aria-label={`Close ${session.name}`}
|
|
onClick={() => destroyWidgetSession(session.key)}
|
|
>
|
|
<Icon size="100" src={Icons.Cross} />
|
|
</IconButton>
|
|
</Box>
|
|
);
|
|
}
|