From 78cb3ffdb85aa6e3b777a090d1cf7664adf3948d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Wed, 9 Sep 2026 16:36:09 +0200 Subject: [PATCH] [client] Move the peer send actions into a footer behind a single Send menu The two send buttons at the top of the peer details panel become one Send button in a bottom bar, matching the Resources tab, with a menu offering File and Clipboard. The clipboard entry is read on open and disabled when empty, mirroring the Files page picker, and Escape closes only the menu instead of the whole panel. --- .../main/advanced/peers/PeerDetailPanel.tsx | 142 ++++++++++++------ client/ui/i18n/locales/de/common.json | 9 ++ client/ui/i18n/locales/en/common.json | 12 ++ client/ui/i18n/locales/es/common.json | 9 ++ client/ui/i18n/locales/fr/common.json | 9 ++ client/ui/i18n/locales/hu/common.json | 9 ++ client/ui/i18n/locales/it/common.json | 9 ++ client/ui/i18n/locales/ja/common.json | 9 ++ client/ui/i18n/locales/pt/common.json | 9 ++ client/ui/i18n/locales/ru/common.json | 9 ++ client/ui/i18n/locales/uk/common.json | 9 ++ client/ui/i18n/locales/zh-CN/common.json | 9 ++ 12 files changed, 200 insertions(+), 44 deletions(-) diff --git a/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx b/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx index 0605206c4..4ab80ff7f 100644 --- a/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx +++ b/client/ui/frontend/src/modules/main/advanced/peers/PeerDetailPanel.tsx @@ -23,6 +23,7 @@ import { ClipboardIcon, ClockIcon, Copy as CopyIcon, + FileIcon, GaugeIcon, HandshakeIcon, KeyRoundIcon, @@ -32,13 +33,19 @@ import { MonitorIcon, Radio, RefreshCwIcon, - SendIcon, + UploadIcon, WaypointsIcon, } from "lucide-react"; import { FileDrop } from "@bindings/services"; import type { PeerStatus } from "@bindings/services/models.js"; import { Button } from "@/components/buttons/Button"; import { ClipboardPreview } from "@/components/ClipboardPreview"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/DropdownMenu"; import { useNavSection } from "@/contexts/NavSectionContext"; import { cn } from "@/lib/cn"; import { CopyToClipboard } from "@/components/CopyToClipboard"; @@ -256,10 +263,6 @@ export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => { - {/* Keyed on the peer so a staged clipboard - confirmation cannot carry over to a different - recipient when the selection changes. */} - { /> + {/* Keyed on the peer so a staged clipboard confirmation + cannot carry over to a different recipient when the + selection changes. */} + )} @@ -286,6 +293,8 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => { const { t } = useTranslation(); const { setSection } = useNavSection(); const { setSelected } = usePeerDetail(); + const [menuOpen, setMenuOpen] = useState(false); + const [clipboard, setClipboard] = useState(""); const [error, setError] = useState(null); const [pendingText, setPendingText] = useState(null); // Deliberately not gated on connStatus: an idle peer is the normal resting @@ -294,12 +303,28 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => { // to dial. const canSend = peer.ip !== ""; + useEffect(() => { + if (!menuOpen) return; + let cancelled = false; + FileDrop.ClipboardText() + .then((text) => { + if (!cancelled) setClipboard(text ?? ""); + }) + .catch(() => { + if (!cancelled) setClipboard(""); + }); + return () => { + cancelled = true; + }; + }, [menuOpen]); + const finishSend = () => { setSelected(null); setSection("files"); }; const sendFiles = async () => { + setMenuOpen(false); setError(null); try { const paths = await FileDrop.PickFiles(); @@ -313,18 +338,14 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => { // Clipboard text is shown for confirmation first: sending it blind gave no // way to tell what was about to leave the machine. - const stageClipboard = async () => { + const stageClipboard = () => { + setMenuOpen(false); setError(null); - try { - const text = await FileDrop.ClipboardText(); - if (!text) { - setError(t("peers.details.sendClipboard.empty")); - return; - } - setPendingText(text); - } catch (e) { - setError(String(e)); + if (!clipboard) { + setError(t("peers.details.sendClipboard.empty")); + return; } + setPendingText(clipboard); }; // Sends the staged text rather than re-reading, so what was confirmed is @@ -342,40 +363,65 @@ const PeerSendActions = ({ peer }: { peer: PeerStatus }) => { }; return ( -
+
{pendingText === null ? ( -
- - +
+ + + + + e.stopPropagation()} + > + void sendFiles()}> +
+ + + {t("peers.details.sendMenu.file")} + +
+
+ +
+ + + {t("peers.details.sendMenu.clipboard")} + + {!clipboard && ( + + {t("peers.details.sendClipboard.empty")} + + )} +
+
+
+
) : (
-
- +
+
)} diff --git a/client/ui/i18n/locales/de/common.json b/client/ui/i18n/locales/de/common.json index aef295f1b..295a9be0a 100644 --- a/client/ui/i18n/locales/de/common.json +++ b/client/ui/i18n/locales/de/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "Zwischenablage ist leer" }, + "peers.details.send": { + "message": "Senden" + }, + "peers.details.sendMenu.file": { + "message": "Datei…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Zwischenablage" + }, "settings.tabs.fileSharing": { "message": "Dateifreigabe" }, diff --git a/client/ui/i18n/locales/en/common.json b/client/ui/i18n/locales/en/common.json index beb61da63..3832f504f 100644 --- a/client/ui/i18n/locales/en/common.json +++ b/client/ui/i18n/locales/en/common.json @@ -1991,6 +1991,18 @@ "message": "Clipboard is empty", "description": "Error when the clipboard has no text." }, + "peers.details.send": { + "message": "Send", + "description": "Label of the single send button at the bottom of the peer details panel; opens the what-to-send menu. Keep short." + }, + "peers.details.sendMenu.file": { + "message": "File…", + "description": "Send menu entry opening the file picker. Keep short." + }, + "peers.details.sendMenu.clipboard": { + "message": "Clipboard", + "description": "Send menu entry sending the clipboard text. Keep short." + }, "settings.tabs.fileSharing": { "message": "File sharing", "description": "Settings tab for file transfer policy." diff --git a/client/ui/i18n/locales/es/common.json b/client/ui/i18n/locales/es/common.json index b1cff6f17..f2e5f07b8 100644 --- a/client/ui/i18n/locales/es/common.json +++ b/client/ui/i18n/locales/es/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "El portapapeles está vacío" }, + "peers.details.send": { + "message": "Enviar" + }, + "peers.details.sendMenu.file": { + "message": "Archivo…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Portapapeles" + }, "settings.tabs.fileSharing": { "message": "Compartir archivos" }, diff --git a/client/ui/i18n/locales/fr/common.json b/client/ui/i18n/locales/fr/common.json index 24ef53356..117bbb984 100644 --- a/client/ui/i18n/locales/fr/common.json +++ b/client/ui/i18n/locales/fr/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "Le presse-papiers est vide" }, + "peers.details.send": { + "message": "Envoyer" + }, + "peers.details.sendMenu.file": { + "message": "Fichier…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Presse-papiers" + }, "settings.tabs.fileSharing": { "message": "Partage de fichiers" }, diff --git a/client/ui/i18n/locales/hu/common.json b/client/ui/i18n/locales/hu/common.json index 816252103..8f494f4e3 100644 --- a/client/ui/i18n/locales/hu/common.json +++ b/client/ui/i18n/locales/hu/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "A vágólap üres" }, + "peers.details.send": { + "message": "Küldés" + }, + "peers.details.sendMenu.file": { + "message": "Fájl…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Vágólap" + }, "settings.tabs.fileSharing": { "message": "Fájlmegosztás" }, diff --git a/client/ui/i18n/locales/it/common.json b/client/ui/i18n/locales/it/common.json index 74412a093..999a53d4a 100644 --- a/client/ui/i18n/locales/it/common.json +++ b/client/ui/i18n/locales/it/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "Gli appunti sono vuoti" }, + "peers.details.send": { + "message": "Invia" + }, + "peers.details.sendMenu.file": { + "message": "File…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Appunti" + }, "settings.tabs.fileSharing": { "message": "Condivisione file" }, diff --git a/client/ui/i18n/locales/ja/common.json b/client/ui/i18n/locales/ja/common.json index 8d31daf03..6d4f82e19 100644 --- a/client/ui/i18n/locales/ja/common.json +++ b/client/ui/i18n/locales/ja/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "クリップボードは空です" }, + "peers.details.send": { + "message": "送信" + }, + "peers.details.sendMenu.file": { + "message": "ファイル…" + }, + "peers.details.sendMenu.clipboard": { + "message": "クリップボード" + }, "settings.tabs.fileSharing": { "message": "ファイル共有" }, diff --git a/client/ui/i18n/locales/pt/common.json b/client/ui/i18n/locales/pt/common.json index 01cacd084..caafd4dd2 100644 --- a/client/ui/i18n/locales/pt/common.json +++ b/client/ui/i18n/locales/pt/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "A área de transferência está vazia" }, + "peers.details.send": { + "message": "Enviar" + }, + "peers.details.sendMenu.file": { + "message": "Arquivo…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Área de transferência" + }, "settings.tabs.fileSharing": { "message": "Compartilhamento de arquivos" }, diff --git a/client/ui/i18n/locales/ru/common.json b/client/ui/i18n/locales/ru/common.json index 99069e7a9..e844e1f1b 100644 --- a/client/ui/i18n/locales/ru/common.json +++ b/client/ui/i18n/locales/ru/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "Буфер обмена пуст" }, + "peers.details.send": { + "message": "Отправить" + }, + "peers.details.sendMenu.file": { + "message": "Файл…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Буфер обмена" + }, "settings.tabs.fileSharing": { "message": "Обмен файлами" }, diff --git a/client/ui/i18n/locales/uk/common.json b/client/ui/i18n/locales/uk/common.json index 619154c5d..4aa07c734 100644 --- a/client/ui/i18n/locales/uk/common.json +++ b/client/ui/i18n/locales/uk/common.json @@ -1493,6 +1493,15 @@ "peers.details.sendClipboard.empty": { "message": "Буфер обміну порожній" }, + "peers.details.send": { + "message": "Надіслати" + }, + "peers.details.sendMenu.file": { + "message": "Файл…" + }, + "peers.details.sendMenu.clipboard": { + "message": "Буфер обміну" + }, "settings.tabs.fileSharing": { "message": "Обмін файлами" }, diff --git a/client/ui/i18n/locales/zh-CN/common.json b/client/ui/i18n/locales/zh-CN/common.json index 17adbc366..8bece8f90 100644 --- a/client/ui/i18n/locales/zh-CN/common.json +++ b/client/ui/i18n/locales/zh-CN/common.json @@ -1471,6 +1471,15 @@ "peers.details.sendClipboard.empty": { "message": "剪贴板为空" }, + "peers.details.send": { + "message": "发送" + }, + "peers.details.sendMenu.file": { + "message": "文件…" + }, + "peers.details.sendMenu.clipboard": { + "message": "剪贴板" + }, "settings.tabs.fileSharing": { "message": "文件共享" },