diff --git a/package-lock.json b/package-lock.json index 5729d9c..2063ff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "dependencies": { "@xtr-dev/rondevu-client": "^0.0.4", + "@zxing/library": "^0.21.3", + "qrcode": "^1.5.4", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -1173,6 +1175,52 @@ "integrity": "sha512-hjvCvjUatIxKsEzykDjCjdax9e2/CXFW39EqVmSOdJI4BOySAta6dIjiACL2aPkM/WZI+lmJpY0+qnGcJz3c9g==", "license": "MIT" }, + "node_modules/@zxing/library": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz", + "integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==", + "license": "MIT", + "dependencies": { + "ts-custom-error": "^3.2.1" + }, + "engines": { + "node": ">= 10.4.0" + }, + "optionalDependencies": { + "@zxing/text-encoding": "~0.9.0" + } + }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "license": "(Unlicense OR Apache-2.0)", + "optional": true + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.8.23", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz", @@ -1217,6 +1265,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001753", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", @@ -1238,6 +1295,35 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1270,6 +1356,21 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.244", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", @@ -1277,6 +1378,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1326,6 +1433,19 @@ "node": ">=6" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1351,6 +1471,24 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1383,6 +1521,18 @@ "node": ">=6" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1438,6 +1588,51 @@ "dev": true, "license": "MIT" }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1445,6 +1640,15 @@ "dev": true, "license": "ISC" }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -1474,6 +1678,23 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1509,6 +1730,21 @@ "node": ">=0.10.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/rollup": { "version": "4.52.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", @@ -1570,6 +1806,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1580,6 +1822,41 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -1671,12 +1948,73 @@ } } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } } } } diff --git a/package.json b/package.json index 258fa08..9ccb564 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ }, "dependencies": { "@xtr-dev/rondevu-client": "^0.0.4", + "@zxing/library": "^0.21.3", + "qrcode": "^1.5.4", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/src/App.jsx b/src/App.jsx index 4bd28a4..6e1f0f8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,7 @@ import { useState, useEffect, useRef } from 'react'; import { Rondevu, RondevuClient } from '@xtr-dev/rondevu-client'; +import QRCode from 'qrcode'; +import { BrowserQRCodeReader } from '@zxing/library'; const rdv = new Rondevu({ baseUrl: 'https://rondevu.xtrdev.workers.dev', @@ -23,8 +25,9 @@ const client = new RondevuClient({ function App() { // Step-based state const [step, setStep] = useState(1); // 1: action, 2: method, 3: details, 4: connected - const [action, setAction] = useState(null); // 'create' or 'join' + const [action, setAction] = useState(null); // 'create', 'join', or 'scan' const [method, setMethod] = useState(null); // 'topic', 'peer-id', 'connection-id' + const [qrCodeUrl, setQrCodeUrl] = useState(''); // Connection state const [topic, setTopic] = useState(''); @@ -40,9 +43,14 @@ function App() { const [messages, setMessages] = useState([]); const [messageInput, setMessageInput] = useState(''); const [logs, setLogs] = useState([]); + const [channelReady, setChannelReady] = useState(false); const connectionRef = useRef(null); const dataChannelRef = useRef(null); + const fileInputRef = useRef(null); + const fileTransfersRef = useRef(new Map()); // Track ongoing file transfers + const videoRef = useRef(null); + const scannerRef = useRef(null); useEffect(() => { log('Demo initialized', 'info'); @@ -107,13 +115,25 @@ function App() { const setupDataChannel = (channel) => { dataChannelRef.current = channel; - channel.onmessage = (event) => { - setMessages(prev => [...prev, { - text: event.data, - type: 'received', - timestamp: new Date() - }]); + channel.onopen = () => { + log('Data channel ready', 'success'); + setChannelReady(true); }; + + channel.onclose = () => { + log('Data channel closed', 'info'); + setChannelReady(false); + }; + + channel.onmessage = (event) => { + handleReceivedMessage(event.data); + }; + + // If channel is already open (for channels we create) + if (channel.readyState === 'open') { + log('Data channel ready', 'success'); + setChannelReady(true); + } }; const handleConnect = async () => { @@ -152,30 +172,259 @@ function App() { setConnectedPeer(connection.remotePeerId || 'Waiting...'); setupConnection(connection); + + // Generate QR code if creating a connection + if (action === 'create' && currentConnectionId) { + try { + const qrUrl = await QRCode.toDataURL(currentConnectionId, { + width: 256, + margin: 2, + color: { + dark: '#667eea', + light: '#ffffff' + } + }); + setQrCodeUrl(qrUrl); + } catch (err) { + log(`QR code generation error: ${err.message}`, 'error'); + } + } } catch (error) { log(`Error: ${error.message}`, 'error'); setConnectionStatus('disconnected'); } }; + const startScanning = async () => { + try { + scannerRef.current = new BrowserQRCodeReader(); + log('Starting QR scanner...', 'info'); + + const videoInputDevices = await scannerRef.current.listVideoInputDevices(); + + if (videoInputDevices.length === 0) { + log('No camera found', 'error'); + return; + } + + const selectedDeviceId = videoInputDevices[0].deviceId; + + scannerRef.current.decodeFromVideoDevice( + selectedDeviceId, + videoRef.current, + (result, err) => { + if (result) { + const scannedId = result.getText(); + log(`Scanned: ${scannedId}`, 'success'); + setConnectionId(scannedId); + stopScanning(); + setMethod('connection-id'); + setStep(3); + } + } + ); + } catch (error) { + log(`Scanner error: ${error.message}`, 'error'); + } + }; + + const stopScanning = () => { + if (scannerRef.current) { + scannerRef.current.reset(); + log('Scanner stopped', 'info'); + } + }; + + useEffect(() => { + if (action === 'scan') { + startScanning(); + } + return () => { + stopScanning(); + }; + }, [action]); + const sendMessage = () => { - if (!messageInput || !dataChannelRef.current || dataChannelRef.current.readyState !== 'open') { + if (!messageInput || !channelReady || !dataChannelRef.current) { return; } - dataChannelRef.current.send(messageInput); + const message = { type: 'text', content: messageInput }; + dataChannelRef.current.send(JSON.stringify(message)); setMessages(prev => [...prev, { text: messageInput, + messageType: 'text', type: 'sent', timestamp: new Date() }]); setMessageInput(''); }; + const handleFileSelect = async (event) => { + const file = event.target.files[0]; + if (!file || !channelReady || !dataChannelRef.current) { + return; + } + + const CHUNK_SIZE = 16384; // 16KB chunks + const fileId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + log(`Sending file: ${file.name} (${(file.size / 1024).toFixed(2)} KB)`, 'info'); + + try { + // Send file metadata + const metadata = { + type: 'file-start', + fileId, + name: file.name, + size: file.size, + mimeType: file.type, + chunks: Math.ceil(file.size / CHUNK_SIZE) + }; + dataChannelRef.current.send(JSON.stringify(metadata)); + + // Read and send file in chunks + const reader = new FileReader(); + let offset = 0; + let chunkIndex = 0; + + const readChunk = () => { + const slice = file.slice(offset, offset + CHUNK_SIZE); + reader.readAsArrayBuffer(slice); + }; + + reader.onload = (e) => { + const chunk = { + type: 'file-chunk', + fileId, + index: chunkIndex, + data: Array.from(new Uint8Array(e.target.result)) + }; + dataChannelRef.current.send(JSON.stringify(chunk)); + + offset += CHUNK_SIZE; + chunkIndex++; + + if (offset < file.size) { + readChunk(); + } else { + // Send completion message + const complete = { type: 'file-complete', fileId }; + dataChannelRef.current.send(JSON.stringify(complete)); + + // Add to local messages + setMessages(prev => [...prev, { + messageType: 'file', + file: { + name: file.name, + size: file.size, + mimeType: file.type, + data: file + }, + type: 'sent', + timestamp: new Date() + }]); + + log(`File sent: ${file.name}`, 'success'); + } + }; + + reader.onerror = () => { + log(`Error reading file: ${file.name}`, 'error'); + }; + + readChunk(); + } catch (error) { + log(`Error sending file: ${error.message}`, 'error'); + } + + // Reset file input + event.target.value = ''; + }; + + const handleReceivedMessage = (data) => { + try { + const message = JSON.parse(data); + + if (message.type === 'text') { + setMessages(prev => [...prev, { + text: message.content, + messageType: 'text', + type: 'received', + timestamp: new Date() + }]); + } else if (message.type === 'file-start') { + fileTransfersRef.current.set(message.fileId, { + name: message.name, + size: message.size, + mimeType: message.mimeType, + chunks: new Array(message.chunks), + receivedChunks: 0 + }); + log(`Receiving file: ${message.name}`, 'info'); + } else if (message.type === 'file-chunk') { + const transfer = fileTransfersRef.current.get(message.fileId); + if (transfer) { + transfer.chunks[message.index] = new Uint8Array(message.data); + transfer.receivedChunks++; + } + } else if (message.type === 'file-complete') { + const transfer = fileTransfersRef.current.get(message.fileId); + if (transfer) { + // Combine all chunks + const totalSize = transfer.chunks.reduce((sum, chunk) => sum + chunk.length, 0); + const combined = new Uint8Array(totalSize); + let offset = 0; + for (const chunk of transfer.chunks) { + combined.set(chunk, offset); + offset += chunk.length; + } + + const blob = new Blob([combined], { type: transfer.mimeType }); + + setMessages(prev => [...prev, { + messageType: 'file', + file: { + name: transfer.name, + size: transfer.size, + mimeType: transfer.mimeType, + data: blob + }, + type: 'received', + timestamp: new Date() + }]); + + log(`File received: ${transfer.name}`, 'success'); + fileTransfersRef.current.delete(message.fileId); + } + } + } catch (error) { + // Assume it's a plain text message (backward compatibility) + setMessages(prev => [...prev, { + text: data, + messageType: 'text', + type: 'received', + timestamp: new Date() + }]); + } + }; + + const downloadFile = (file) => { + const url = URL.createObjectURL(file.data); + const a = document.createElement('a'); + a.href = url; + a.download = file.name; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + const reset = () => { if (connectionRef.current) { connectionRef.current.close(); } + stopScanning(); setStep(1); setAction(null); setMethod(null); @@ -187,6 +436,8 @@ function App() { setConnectedPeer(null); setCurrentConnectionId(null); setMessages([]); + setChannelReady(false); + setQrCodeUrl(''); connectionRef.current = null; dataChannelRef.current = null; }; @@ -224,7 +475,7 @@ function App() { {step === 1 && (

Choose Action

-
+
+
+ {action === 'scan' && ( +
+
+ )}
)} @@ -398,13 +664,37 @@ function App() {
+ {qrCodeUrl && connectionStatus === 'connecting' && ( +
+

Scan to connect:

+ Connection QR Code +

{currentConnectionId}

+
+ )} +
{messages.length === 0 ? (

No messages yet. Start chatting!

) : ( messages.map((msg, idx) => (
-
{msg.text}
+ {msg.messageType === 'text' ? ( +
{msg.text}
+ ) : ( +
+
📎
+
+
{msg.file.name}
+
{(msg.file.size / 1024).toFixed(2)} KB
+
+ +
+ )}
{msg.timestamp.toLocaleTimeString()}
)) @@ -412,17 +702,31 @@ function App() {
+ + setMessageInput(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && sendMessage()} placeholder="Type a message..." - disabled={!dataChannelRef.current || dataChannelRef.current.readyState !== 'open'} + disabled={!channelReady} /> diff --git a/src/index.css b/src/index.css index 8b06c11..85abfda 100644 --- a/src/index.css +++ b/src/index.css @@ -103,6 +103,10 @@ body { margin-bottom: 24px; } +.button-grid-three { + grid-template-columns: repeat(3, 1fr); +} + .action-button { background: white; border: 3px solid #e0e0e0; @@ -392,7 +396,7 @@ input[type="text"]:disabled { gap: 12px; } -.message-input input { +.message-input input[type="text"] { flex: 1; margin-bottom: 0; } @@ -409,6 +413,11 @@ input[type="text"]:disabled { transition: all 0.2s; } +.message-input .file-button { + padding: 12px 16px; + font-size: 1.2rem; +} + .message-input button:hover:not(:disabled) { background: #5568d3; transform: translateY(-1px); @@ -421,6 +430,90 @@ input[type="text"]:disabled { transform: none; } +.message-file { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-radius: 12px; + background: inherit; + width: 100%; + max-width: 400px; +} + +.message.sent .message-file { + background: rgba(255, 255, 255, 0.2); + margin-left: auto; +} + +.message.received .message-file { + background: #f8f9fa; + border: 2px solid #e0e0e0; +} + +.file-icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.file-info { + flex: 1; + min-width: 0; +} + +.file-name { + font-weight: 600; + font-size: 0.9rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.message.sent .file-name { + color: white; +} + +.message.received .file-name { + color: #333; +} + +.file-size { + font-size: 0.75rem; + opacity: 0.8; + margin-top: 2px; +} + +.message.sent .file-size { + color: white; +} + +.message.received .file-size { + color: #6c757d; +} + +.file-download { + padding: 6px 12px; + font-size: 0.85rem; + background: rgba(255, 255, 255, 0.9); + color: #667eea; + border: none; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + flex-shrink: 0; +} + +.message.sent .file-download { + background: rgba(255, 255, 255, 0.3); + color: white; +} + +.file-download:hover { + background: white; + transform: scale(1.05); +} + .logs { margin-top: 24px; border-top: 2px solid #f0f0f0; @@ -478,6 +571,57 @@ input[type="text"]:disabled { backdrop-filter: blur(10px); } +.scanner-container { + margin-top: 24px; + text-align: center; + animation: fadeIn 0.3s ease-out; +} + +.scanner-video { + width: 100%; + max-width: 400px; + height: 300px; + border-radius: 12px; + background: #1e1e1e; + margin-bottom: 16px; + object-fit: cover; +} + +.qr-code-container { + text-align: center; + padding: 24px; + background: #f8f9fa; + border-radius: 12px; + margin-bottom: 20px; +} + +.qr-label { + font-size: 0.95rem; + color: #667eea; + font-weight: 600; + margin-bottom: 12px; +} + +.qr-code { + display: block; + margin: 0 auto 12px; + border-radius: 8px; + background: white; + padding: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.connection-id-display { + font-family: 'Courier New', monospace; + font-size: 0.9rem; + color: #333; + background: white; + padding: 8px 16px; + border-radius: 6px; + display: inline-block; + font-weight: 600; +} + .footer { text-align: center; padding: 40px 20px 30px; @@ -517,7 +661,8 @@ input[type="text"]:disabled { padding: 32px 24px; } - .button-grid { + .button-grid, + .button-grid-three { grid-template-columns: 1fr; }