feat(invite): download the room QR code as a PNG
The invite QR was display-only. Add a "Download QR" button that exports it as a PNG via an offscreen high-resolution (1024px, spec 4-module quiet zone) QRCodeCanvas + canvas.toBlob, saved through useSaveFile (filename from the room name) with the standard download toast. The visible code stays an SVG so it renders crisply at any size/theme. Also corrects the stale LOTUS_FEATURES note (the QR is generated locally via qrcode.react, not api.qrserver.com). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -1102,7 +1102,8 @@ Persists via the `homeRoomSort` setting.
|
|||||||
|
|
||||||
`RoomShareInvite.tsx` provides a shareable invite UI:
|
`RoomShareInvite.tsx` provides a shareable invite UI:
|
||||||
|
|
||||||
- 160×160px QR code generated via `api.qrserver.com`
|
- 160×160px QR code generated locally via `qrcode.react` (`QRCodeSVG`) — no third-party service, works offline and under strict CSP, on a white quiet-zone so it scans on any theme
|
||||||
|
- **Download QR**: exports the code as a PNG via an offscreen high-resolution (1024px, spec 4-module margin) `QRCodeCanvas` + `canvas.toBlob`, saved through `useSaveFile` (filename derived from the room name) with the standard download toast
|
||||||
- "Copy Link" button to copy the `matrix.to` URI
|
- "Copy Link" button to copy the `matrix.to` URI
|
||||||
- Also accessible via a toggle button (⊞) in the Invite modal
|
- Also accessible via a toggle button (⊞) in the Invite modal
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import { Box, Button, config, Icon, Icons, Text } from 'folds';
|
import { Box, Button, config, Icon, Icons, Text } from 'folds';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG, QRCodeCanvas } from 'qrcode.react';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
@@ -8,11 +8,22 @@ import { CutoutCard } from '../../../components/cutout-card';
|
|||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { useRoom } from '../../../hooks/useRoom';
|
import { useRoom } from '../../../hooks/useRoom';
|
||||||
import { getMatrixToRoom } from '../../../plugins/matrix-to';
|
import { getMatrixToRoom } from '../../../plugins/matrix-to';
|
||||||
|
import { useSaveFile } from '../../../hooks/useSaveFile';
|
||||||
|
|
||||||
|
// Filesystem-safe basename from an arbitrary room name.
|
||||||
|
function sanitizeFilename(name: string): string {
|
||||||
|
const cleaned = name.replace(/[^a-z0-9\-_ ]+/gi, '').trim().replace(/\s+/g, '-');
|
||||||
|
return cleaned || 'room';
|
||||||
|
}
|
||||||
|
|
||||||
export function RoomShareInvite() {
|
export function RoomShareInvite() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const room = useRoom();
|
const room = useRoom();
|
||||||
|
const saveFile = useSaveFile();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
// Offscreen high-resolution canvas used only to export the QR as a PNG; the
|
||||||
|
// visible code stays an SVG so it renders crisply at any size/theme.
|
||||||
|
const qrCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
const domain = mx.getDomain() ?? undefined;
|
const domain = mx.getDomain() ?? undefined;
|
||||||
const inviteUrl = getMatrixToRoom(room.roomId, domain ? [domain] : undefined);
|
const inviteUrl = getMatrixToRoom(room.roomId, domain ? [domain] : undefined);
|
||||||
@@ -24,6 +35,14 @@ export function RoomShareInvite() {
|
|||||||
});
|
});
|
||||||
}, [inviteUrl]);
|
}, [inviteUrl]);
|
||||||
|
|
||||||
|
const handleDownloadQr = useCallback(() => {
|
||||||
|
const canvas = qrCanvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) saveFile(blob, `${sanitizeFilename(room.name)}-invite-qr.png`);
|
||||||
|
}, 'image/png');
|
||||||
|
}, [saveFile, room.name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
className={SequenceCardStyle}
|
className={SequenceCardStyle}
|
||||||
@@ -62,7 +81,7 @@ export function RoomShareInvite() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box justifyContent="Center">
|
<Box direction="Column" alignItems="Center" gap="300">
|
||||||
{/* Generated locally (qrcode.react) — no third-party service, works
|
{/* Generated locally (qrcode.react) — no third-party service, works
|
||||||
offline + under strict CSP. White padded quiet-zone so the
|
offline + under strict CSP. White padded quiet-zone so the
|
||||||
default black-on-white code scans on any theme. */}
|
default black-on-white code scans on any theme. */}
|
||||||
@@ -76,9 +95,28 @@ export function RoomShareInvite() {
|
|||||||
>
|
>
|
||||||
<QRCodeSVG value={inviteUrl} size={160} level="M" title="Room invite QR code" />
|
<QRCodeSVG value={inviteUrl} size={160} level="M" title="Room invite QR code" />
|
||||||
</Box>
|
</Box>
|
||||||
|
<Button
|
||||||
|
size="300"
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Soft"
|
||||||
|
radii="300"
|
||||||
|
onClick={handleDownloadQr}
|
||||||
|
before={<Icon size="100" src={Icons.Download} />}
|
||||||
|
>
|
||||||
|
<Text size="B300">Download QR</Text>
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</CutoutCard>
|
</CutoutCard>
|
||||||
|
{/* Offscreen, high-res (spec 4-module quiet zone) source for PNG export. */}
|
||||||
|
<QRCodeCanvas
|
||||||
|
ref={qrCanvasRef}
|
||||||
|
value={inviteUrl}
|
||||||
|
size={1024}
|
||||||
|
level="M"
|
||||||
|
marginSize={4}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user