import React, { useCallback, useRef, useState } from 'react'; import { Box, Button, color, config, Icon, Icons, Text } from 'folds'; 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'; import { CutoutCard } from '../../../components/cutout-card'; import { useRoom } from '../../../hooks/useRoom'; import { getMatrixToRoom } from '../../../plugins/matrix-to'; import { getViaServers } from '../../../plugins/via-servers'; import { useSaveFile } from '../../../hooks/useSaveFile'; // Filesystem-safe basename from an arbitrary room name. Denylist only the // path-hostile / control characters so Unicode names (CJK, Cyrillic, emoji, …) // survive instead of being stripped to the 'room' fallback. Collapse whitespace, // trim separator runs, and cap length to stay under filesystem name limits. // eslint-disable-next-line no-control-regex const UNSAFE_FILENAME = /[/\\:*?"<>|-]+/g; function sanitizeFilename(name: string): string { const cleaned = name .replace(UNSAFE_FILENAME, '') .replace(/\s+/g, '-') .replace(/^[-.]+|[-.]+$/g, '') .slice(0, 80) .replace(/[-.]+$/g, ''); return cleaned || 'room'; } export function RoomShareInvite() { const room = useRoom(); const saveFile = useSaveFile(); const [copied, setCopied] = useState(false); const [qrError, setQrError] = 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); // Use the shared via-server resolver (creator's/highest-power server plus the // top-populated member servers) instead of only the local homeserver, so the // link keeps resolving even if the sharer's own server later goes offline. const inviteUrl = getMatrixToRoom(room.roomId, getViaServers(room)); const handleCopy = useCallback(() => { navigator.clipboard.writeText(inviteUrl).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); }, [inviteUrl]); const handleDownloadQr = useCallback(() => { const canvas = qrCanvasRef.current; if (!canvas) return; setQrError(false); canvas.toBlob((blob) => { if (blob) { saveFile(blob, `${sanitizeFilename(room.name)}-invite-qr.png`); } else { // PNG encoding failed — surface it so the user isn't left re-clicking a // button that silently did nothing. setQrError(true); setTimeout(() => setQrError(false), 4000); } }, 'image/png'); }, [saveFile, room.name]); return ( {inviteUrl} {/* 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. */} {qrError && ( Could not generate the QR image. Try again. )} {/* Offscreen, high-res (spec 4-module quiet zone) source for PNG export. */} ); }