fix/polish: wave 1+2 improvements across six features
CI / Build & Quality Checks (push) Successful in 10m25s

B1 - GIF upload progress: spinner on GIF button + disabled state while
     fetch+upload is in flight; clears on success or error
B2 - PiP position persistence: drag end saves left/top to localStorage;
     entering PiP restores saved position (clamped to current viewport)
B3 - PiP snap-to-corner: double-click the PiP overlay snaps to nearest
     corner with a 180ms CSS transition; saves new position
B4 - Device sessions loading state: useOtherUserDevices now returns
     {status:'loading'|'error'|'success', devices} instead of bare
     array; UserDeviceSessions shows spinner while loading
B5 - Device sessions error state: catch in hook sets status:'error';
     panel shows warning icon + 'Could not load sessions' message
B6 - Screenshare fullscreen Safari guard: hide button when
     document.fullscreenEnabled is false (iOS Safari, some mobile)
B7 - Status save error: show critical-coloured error text below Save
     button when saveState.status === AsyncStatus.Error
B9 - Encrypted search coverage counter: 'X / Y cached' badge next to
     'Encrypted Rooms' heading using existing localResult fields
D2 - PiP screenshare spotlight: track auto-spotlight in a ref; release
     spotlight when screenshare ends while in PiP mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 17:13:54 -04:00
parent e2b8e162e3
commit 403ec3d80c
7 changed files with 132 additions and 38 deletions
+1 -1
View File
@@ -360,7 +360,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
screenshare ? callEmbed.control.toggleScreenshare() : setShareConfirm(true)
}
/>
{screenshare && (
{screenshare && !!document.fullscreenEnabled && (
<FullscreenButton isFullscreen={isFullscreen} onToggle={handleFullscreen} />
)}
</Box>
@@ -508,6 +508,9 @@ export function MessageSearch({
<Box alignItems="Center" gap="200">
<Icon size="200" src={Icons.Lock} />
<Text size="H5">Encrypted Rooms</Text>
<Text size="T200" style={{ opacity: 0.55 }}>
{`${localResult.searchedRoomsCount} / ${localResult.encryptedRoomsCount} cached`}
</Text>
</Box>
<Text size="T300" priority="300">
{localResult.groups.length > 0
+21 -12
View File
@@ -299,6 +299,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const gifBtnRef = useRef<HTMLButtonElement>(null);
const [hideStickerBtn, setHideStickerBtn] = useState(document.body.clientWidth < 500);
const [gifError, setGifError] = React.useState<string | null>(null);
const [gifUploading, setGifUploading] = React.useState(false);
const isComposing = useComposingCheck();
@@ -519,6 +520,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const handleGifSelect = useCallback(
async (gifUrl: string, w: number, h: number) => {
setGifUploading(true);
try {
// Only fetch from trusted Giphy CDN domains
const allowed = [
@@ -562,6 +564,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
if (!alive()) return;
setGifError('Failed to send GIF. Please try again.');
setTimeout(() => setGifError(null), 4000);
} finally {
if (alive()) setGifUploading(false);
}
},
[mx, roomId, alive],
@@ -840,22 +844,27 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
ref={gifBtnRef}
aria-label="Insert GIF"
aria-pressed={gifOpen}
onClick={() => setGifOpen(!gifOpen)}
onClick={() => !gifUploading && setGifOpen(!gifOpen)}
variant="SurfaceVariant"
size="300"
radii="300"
disabled={gifUploading}
>
<Text
size="T200"
style={{
fontWeight: 800,
fontSize: '11px',
letterSpacing: '0.04em',
lineHeight: 1,
}}
>
GIF
</Text>
{gifUploading ? (
<Spinner variant="Secondary" size="100" />
) : (
<Text
size="T200"
style={{
fontWeight: 800,
fontSize: '11px',
letterSpacing: '0.04em',
lineHeight: 1,
}}
>
GIF
</Text>
)}
</IconButton>
</PopOut>
)}
@@ -508,6 +508,11 @@ function ProfileStatus() {
<Text size="B400">Save</Text>
</Button>
</Box>
{saveState.status === AsyncStatus.Error && (
<Text size="T200" style={{ color: 'var(--tc-critical-normal)' }}>
Failed to save status server may be rate limiting. Try again.
</Text>
)}
<Box alignItems="Center" gap="200">
<Text size="T200" style={{ opacity: 0.6, whiteSpace: 'nowrap', flexShrink: 0 }}>
Auto-clear after: