From 99e580039f450cc3cf145cdf1364f9dcedf0372b Mon Sep 17 00:00:00 2001 From: SamWang Date: Wed, 22 Jul 2026 11:17:13 +0800 Subject: [PATCH 1/4] fix(frontend): ask before resuming a stored session when the URL names another MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A saved session made `sessionData` truthy on load, so ClipboardInterface rendered and SessionEntry never mounted — the connection id in the URL was never read. Scanning a QR for a new session therefore resumed the old one and the only prompt shown was that old session's expiry check. Resolve the collision in AppContent before either view renders: probe the stored session, then ask whether to open the id from the URL, striking the old id out when it no longer exists. Opening the new one clears the stored session so SessionEntry's existing URL-join path handles it; staying rewrites the URL back so a reload does not re-prompt. --- frontend/src/App.jsx | 53 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e7db8af..0652881 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,22 +1,61 @@ import React, {useEffect, useState} from 'react'; import {SessionProvider, useSession} from './context/SessionContext'; import {ToastProvider} from './context/ToastContext'; -import {ConfirmProvider} from './context/ConfirmContext'; +import {ConfirmProvider, useConfirm} from './context/ConfirmContext'; import {SessionEntry} from './components/SessionEntry'; import {ClipboardInterface} from './components/ClipboardInterface'; +import {getSession} from './utils/api'; import {initConfig} from './utils/config'; +// The id in the URL, if the path looks like one. `/r//` raw links and +// anything else with a slash are excluded by the character class. +function urlSessionId() { + const id = window.location.pathname.replace(/^\//, '').trim().toLowerCase(); + return /^[a-z0-9]+$/.test(id) ? id : null; +} + function AppContent() { - const {sessionData} = useSession(); + const {sessionData, clearSession} = useSession(); + const confirm = useConfirm(); const [isReady, setIsReady] = useState(false); useEffect(() => { - initConfig() - .then(() => setIsReady(true)) - .catch((err) => { + (async () => { + try { + await initConfig(); + } catch (err) { console.error('Failed to initialize:', err); - setIsReady(true); - }); + } + // A saved session plus a *different* id in the URL — scanning a new + // QR while an old session is still stored — is ambiguous. Without + // this the stored session always won, the URL was ignored, and the + // only thing the user saw was the old session reported as expired. + const newId = urlSessionId(); + if (sessionData && newId && newId !== sessionData.connection_id) { + const oldId = sessionData.connection_id; + const oldAlive = await getSession(oldId, sessionData.user_id).then(() => true, () => false); + const shown = oldAlive ? {oldId} : {oldId}; + const goNew = await confirm({ + title: 'Open the new connection?', + message: ( + <> + You already have connection {shown}{oldAlive ? '' : ' open but expired'}. + {' '}Open {newId} instead? + + ), + confirmText: 'Open new', + cancelText: 'Stay', + confirmStyle: 'primary', + }); + if (goNew) { + clearSession(); // SessionEntry joins from the URL on mount + } else { + window.history.replaceState({}, '', `/${oldId}`); + } + } + setIsReady(true); + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (!isReady) { From ce49c21ae92f9c4cd7288fdcab1001e133d98ab5 Mon Sep 17 00:00:00 2001 From: SamWang Date: Wed, 22 Jul 2026 12:03:29 +0800 Subject: [PATCH 2/4] chore: bump version to 2.0.1 Every place the version is stated: both READMEs' badges, the FastAPI app metadata, backend pyproject, frontend package.json, the footer and the menu, and the two lockfiles' entries for the project's own package. --- README.md | 2 +- README.zh-TW.md | 2 +- backend/app.py | 2 +- backend/pyproject.toml | 2 +- backend/uv.lock | 2 +- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- frontend/src/App.jsx | 2 +- frontend/src/components/Menu.jsx | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ae1e27f..0dd1e0d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Clippy - + A web-based application that allows users to share text and files in real-time through secure, encrypted sessions. diff --git a/README.zh-TW.md b/README.zh-TW.md index 19880f8..175feea 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -4,7 +4,7 @@ # Clippy - + 一個讓使用者能透過安全、加密的的方式,即時分享文字與檔案的網頁應用程式。 diff --git a/backend/app.py b/backend/app.py index c68de7e..666e50d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -138,7 +138,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="Clippy API", description="Secure collaborative clipboard with real-time file and text sharing", - version="2.0.0", + version="2.0.1", openapi_url="/api/v2/openapi.json" if ENABLE_DOCS else None, docs_url="/api/v2/docs" if ENABLE_DOCS else None, redoc_url=None, diff --git a/backend/pyproject.toml b/backend/pyproject.toml index ff15dec..717d894 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "clippy-backend" -version = "2.0.0" +version = "2.0.1" description = "Clippy - Collaborative clipboard backend" requires-python = ">=3.12" dependencies = [ diff --git a/backend/uv.lock b/backend/uv.lock index 4a14cdd..9603bad 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -150,7 +150,7 @@ wheels = [ [[package]] name = "clippy-backend" -version = "2.0.0" +version = "2.0.1" source = { virtual = "." } dependencies = [ { name = "aiofiles" }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 641bfe0..72d217d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "2.0.0", + "version": "2.0.1", "dependencies": { "highlight.js": "^11.11.1", "qrcode": "^1.5.4", diff --git a/frontend/package.json b/frontend/package.json index 5091a8a..b73daaf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.0.0", + "version": "2.0.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0652881..7c29a67 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -73,7 +73,7 @@ function AppContent() { {!sessionData && (