diff --git a/.github/workflows/check_version.yaml b/.github/workflows/check_version.yaml index 51fa72f..29755fb 100644 --- a/.github/workflows/check_version.yaml +++ b/.github/workflows/check_version.yaml @@ -21,16 +21,12 @@ jobs: backend_toml=$(grep -Po '(?<=^version = ")[^"]+' backend/pyproject.toml) backend_app=$(grep -Po "(?<=version=\")[^\"]+" backend/app.py) frontend_pkg=$(node -p "require('./frontend/package.json').version") - menu_jsx=$(grep -Po "(?<=APP_VERSION = 'v)[^']+" frontend/src/components/Menu.jsx) - app_jsx=$(grep -Po '(?<=Clippy v)[0-9]+\.[0-9]+\.[0-9]+' frontend/src/App.jsx) readme_en=$(grep -Po '(?<=Version-v)[0-9]+\.[0-9]+\.[0-9]+' README.md) readme_tw=$(grep -Po '(?<=Version-v)[0-9]+\.[0-9]+\.[0-9]+' README.zh-TW.md) echo "backend_toml=$backend_toml" echo "backend_app=$backend_app" echo "frontend_pkg=$frontend_pkg" - echo "menu_jsx=$menu_jsx" - echo "app_jsx=$app_jsx" echo "readme_en=$readme_en" echo "readme_tw=$readme_tw" @@ -38,16 +34,12 @@ jobs: if [ "$backend_toml" != "$backend_app" ] || \ [ "$backend_toml" != "$frontend_pkg" ] || \ - [ "$backend_toml" != "$menu_jsx" ] || \ - [ "$backend_toml" != "$app_jsx" ] || \ [ "$backend_toml" != "$readme_en" ] || \ [ "$backend_toml" != "$readme_tw" ]; then echo "::error::Version mismatch detected!" echo " backend/pyproject.toml: $backend_toml" echo " backend/app.py: $backend_app" echo " frontend/package.json: $frontend_pkg" - echo " Menu.jsx: $menu_jsx" - echo " App.jsx: $app_jsx" echo " README.md: $readme_en" echo " README.zh-TW.md: $readme_tw" exit 1 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/eslint.config.js b/frontend/eslint.config.js index e569546..b09eeb5 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -40,6 +40,8 @@ export default [ requestAnimationFrame: 'readonly', cancelAnimationFrame: 'readonly', getComputedStyle: 'readonly', + // Injected at build time by vite.config.js `define`. + __APP_VERSION__: 'readonly', }, }, plugins: { 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 e7db8af..2644e68 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) { @@ -34,7 +73,7 @@ function AppContent() { {!sessionData && (