From a1107015fd84957efb0834005dc72d0a8e7df0d4 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Fri, 10 Jul 2026 15:57:19 -0400 Subject: [PATCH] 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 --- LOTUS_FEATURES.md | 3 +- .../general/RoomShareInvite.tsx | 44 +++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md index 91a008cc9..8bb25f6d8 100644 --- a/LOTUS_FEATURES.md +++ b/LOTUS_FEATURES.md @@ -1102,7 +1102,8 @@ Persists via the `homeRoomSort` setting. `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 - Also accessible via a toggle button (⊞) in the Invite modal diff --git a/src/app/features/common-settings/general/RoomShareInvite.tsx b/src/app/features/common-settings/general/RoomShareInvite.tsx index 8377d1d77..f3b8189ba 100644 --- a/src/app/features/common-settings/general/RoomShareInvite.tsx +++ b/src/app/features/common-settings/general/RoomShareInvite.tsx @@ -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 { QRCodeSVG } from 'qrcode.react'; +import { QRCodeSVG, QRCodeCanvas } from 'qrcode.react'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SettingTile } from '../../../components/setting-tile'; @@ -8,11 +8,22 @@ import { CutoutCard } from '../../../components/cutout-card'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useRoom } from '../../../hooks/useRoom'; 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() { const mx = useMatrixClient(); const room = useRoom(); + const saveFile = useSaveFile(); 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(null); const domain = mx.getDomain() ?? undefined; const inviteUrl = getMatrixToRoom(room.roomId, domain ? [domain] : undefined); @@ -24,6 +35,14 @@ export function RoomShareInvite() { }); }, [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 ( - + {/* Generated locally (qrcode.react) — no third-party service, works offline + under strict CSP. White padded quiet-zone so the default black-on-white code scans on any theme. */} @@ -76,9 +95,28 @@ export function RoomShareInvite() { > + + {/* Offscreen, high-res (spec 4-module quiet zone) source for PNG export. */} + ); }