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 && (