From eb612d47c0a5b45d5996cef4e2245c2a18559f2b Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Wed, 9 Apr 2025 14:55:12 +0200 Subject: [PATCH 1/2] Allow copy and paste on logins - allow shell open for linking - some platforms it's not working as expected --- .../capabilities/main-capability.json | 3 +- .../src-tauri/gen/schemas/capabilities.json | 2 +- .../Login/LoginPasswordFormWrapper.tsx | 117 +++++++++++++++++ .../Login/MnemonicLoginFormWrapper.tsx | 123 ++++++++++++++++++ .../src/pages/auth/pages/signin-mnemonic.tsx | 8 +- .../src/pages/auth/pages/signin-password.tsx | 7 +- 6 files changed, 253 insertions(+), 7 deletions(-) create mode 100644 nym-wallet/src/components/Login/LoginPasswordFormWrapper.tsx create mode 100644 nym-wallet/src/components/Login/MnemonicLoginFormWrapper.tsx diff --git a/nym-wallet/src-tauri/capabilities/main-capability.json b/nym-wallet/src-tauri/capabilities/main-capability.json index 9ca4de883f..60b91143bb 100644 --- a/nym-wallet/src-tauri/capabilities/main-capability.json +++ b/nym-wallet/src-tauri/capabilities/main-capability.json @@ -32,6 +32,7 @@ "updater:allow-download", "updater:allow-download-and-install", "updater:allow-install", - "core:event:allow-listen" + "core:event:allow-listen", + "shell:allow-open" ] } \ No newline at end of file diff --git a/nym-wallet/src-tauri/gen/schemas/capabilities.json b/nym-wallet/src-tauri/gen/schemas/capabilities.json index 533103f942..a0a9361c59 100644 --- a/nym-wallet/src-tauri/gen/schemas/capabilities.json +++ b/nym-wallet/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"main-capability":{"identifier":"main-capability","description":"Default capability for Nym Wallet main window","local":true,"windows":["main","nymWalletApp","log"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","opener:allow-open-url","opener:allow-default-urls","core:window:allow-set-title","core:app:allow-version","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","updater:default","updater:allow-check","updater:allow-download","updater:allow-download-and-install","updater:allow-install","core:event:allow-listen"],"platforms":["linux","macOS","windows"]}} \ No newline at end of file +{"main-capability":{"identifier":"main-capability","description":"Default capability for Nym Wallet main window","local":true,"windows":["main","nymWalletApp","log"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","opener:allow-open-url","opener:allow-default-urls","core:window:allow-set-title","core:app:allow-version","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","updater:default","updater:allow-check","updater:allow-download","updater:allow-download-and-install","updater:allow-install","core:event:allow-listen","shell:allow-open"],"platforms":["linux","macOS","windows"]}} \ No newline at end of file diff --git a/nym-wallet/src/components/Login/LoginPasswordFormWrapper.tsx b/nym-wallet/src/components/Login/LoginPasswordFormWrapper.tsx new file mode 100644 index 0000000000..208df3f631 --- /dev/null +++ b/nym-wallet/src/components/Login/LoginPasswordFormWrapper.tsx @@ -0,0 +1,117 @@ +import React, { useRef, useEffect } from 'react'; +import { Box } from '@mui/material'; +import { PasswordInput } from '@nymproject/react/textfields/Password'; +import { readText } from '@tauri-apps/plugin-clipboard-manager'; +import ContentPasteIcon from '@mui/icons-material/ContentPaste'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; + +interface PasteButtonProps { + onPaste: () => void; +} + +const PasteButton: React.FC = ({ onPaste }) => ( + + + + + +); + +interface EnhancedPasswordInputProps { + password: string; + onUpdatePassword: (password: string) => void; + label?: string; + placeholder?: string; + error?: string; + autoFocus?: boolean; + disabled?: boolean; + [key: string]: any; +} + +export const EnhancedPasswordInput: React.FC = ({ + password, + onUpdatePassword, + ...otherProps +}) => { + const inputRef = useRef(null); + + useEffect(() => { + const findInputElement = () => { + if (!inputRef.current) return undefined; + + const input = inputRef.current.querySelector('input'); + if (!input) return undefined; + + const handleKeyDown = async (e: KeyboardEvent) => { + if (document.activeElement !== input) return; + + if ((e.metaKey || e.ctrlKey) && e.key === 'a') { + e.preventDefault(); + setTimeout(() => { + (input as HTMLInputElement).select(); + }, 0); + } + + if ((e.metaKey || e.ctrlKey) && e.key === 'v') { + e.preventDefault(); + try { + const clipboardText = await readText(); + if (clipboardText) { + onUpdatePassword(clipboardText); + } + } catch (err) { + console.error('Failed to paste text:', err); + } + } + }; + + input.addEventListener('keydown', handleKeyDown); + + return () => { + input.removeEventListener('keydown', handleKeyDown); + }; + }; + + const cleanup = findInputElement(); + const timeoutId = setTimeout(findInputElement, 100); + + return () => { + if (cleanup) cleanup(); + clearTimeout(timeoutId); + }; + }, [onUpdatePassword]); + + const handlePaste = async () => { + try { + const clipboardText = await readText(); + if (clipboardText) { + onUpdatePassword(clipboardText); + + const input = inputRef.current?.querySelector('input'); + if (input) { + input.focus(); + } + } + } catch (err) { + console.error('Failed to paste from clipboard:', err); + } + }; + + return ( + + + + + + + ); +}; diff --git a/nym-wallet/src/components/Login/MnemonicLoginFormWrapper.tsx b/nym-wallet/src/components/Login/MnemonicLoginFormWrapper.tsx new file mode 100644 index 0000000000..30dcd105b4 --- /dev/null +++ b/nym-wallet/src/components/Login/MnemonicLoginFormWrapper.tsx @@ -0,0 +1,123 @@ +import React, { useRef, useEffect } from 'react'; +import { Box } from '@mui/material'; +import { MnemonicInput as OriginalMnemonicInput } from '@nymproject/react/textfields/Mnemonic'; +import { readText } from '@tauri-apps/plugin-clipboard-manager'; +import ContentPasteIcon from '@mui/icons-material/ContentPaste'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; + +interface PasteButtonProps { + onPaste: () => void; +} + +const PasteButton: React.FC = ({ onPaste }) => ( + + + + + +); + +interface EnhancedMnemonicInputProps { + mnemonic: string; + onUpdateMnemonic: (mnemonic: string) => void; + error?: string; + [key: string]: any; +} + +export { OriginalMnemonicInput as MnemonicInput }; + +export const EnhancedMnemonicInput: React.FC = ({ + mnemonic, + onUpdateMnemonic, + ...otherProps +}) => { + const inputRef = useRef(null); + + useEffect(() => { + const findInputElement = () => { + if (!inputRef.current) return undefined; + + const textarea = inputRef.current.querySelector('textarea'); + const input = textarea || inputRef.current.querySelector('input'); + + if (!input) return undefined; + + // Fix the event type issue by casting Event to KeyboardEvent + const handleKeyDown = async (e: Event) => { + const keyEvent = e as KeyboardEvent; + if (document.activeElement !== input) return; + + if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'a') { + keyEvent.preventDefault(); + setTimeout(() => { + if (textarea) { + textarea.select(); + } else { + (input as HTMLInputElement).select(); + } + }, 0); + } + + if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'v') { + keyEvent.preventDefault(); + try { + const clipboardText = await readText(); + if (clipboardText) { + onUpdateMnemonic(clipboardText.trim()); + } + } catch (err) { + console.error('Failed to paste text:', err); + } + } + }; + + input.addEventListener('keydown', handleKeyDown); + + return () => { + input.removeEventListener('keydown', handleKeyDown); + }; + }; + + const cleanup = findInputElement(); + const timeoutId = setTimeout(findInputElement, 100); + + return () => { + if (cleanup) cleanup(); + clearTimeout(timeoutId); + }; + }, [onUpdateMnemonic]); + + const handlePaste = async () => { + try { + const clipboardText = await readText(); + if (clipboardText) { + onUpdateMnemonic(clipboardText.trim()); + + const textarea = inputRef.current?.querySelector('textarea'); + const input = textarea || inputRef.current?.querySelector('input'); + if (input) { + input.focus(); + } + } + } catch (err) { + console.error('Failed to paste from clipboard:', err); + } + }; + + return ( + + + + + + + ); +}; diff --git a/nym-wallet/src/pages/auth/pages/signin-mnemonic.tsx b/nym-wallet/src/pages/auth/pages/signin-mnemonic.tsx index 55f34774b5..a5babd3a96 100644 --- a/nym-wallet/src/pages/auth/pages/signin-mnemonic.tsx +++ b/nym-wallet/src/pages/auth/pages/signin-mnemonic.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { Box, Button, FormControl, Stack } from '@mui/material'; import { AppContext } from 'src/context'; import { isPasswordCreated } from 'src/requests'; -import { MnemonicInput } from '@nymproject/react/textfields/Mnemonic'; +import { EnhancedMnemonicInput } from 'src/components/Login/MnemonicLoginFormWrapper'; import { Subtitle } from '../components'; export const SignInMnemonic = () => { @@ -38,7 +38,11 @@ export const SignInMnemonic = () => { }} > - setMnemonic(mnc)} error={error} /> + setMnemonic(mnc)} + error={error} + /> diff --git a/nym-wallet/src/pages/auth/pages/signin-password.tsx b/nym-wallet/src/pages/auth/pages/signin-password.tsx index 0b62040ac6..45e9c4602b 100644 --- a/nym-wallet/src/pages/auth/pages/signin-password.tsx +++ b/nym-wallet/src/pages/auth/pages/signin-password.tsx @@ -1,7 +1,7 @@ import React, { useContext, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Button, FormControl, Stack } from '@mui/material'; -import { PasswordInput } from '@nymproject/react/textfields/Password'; +import { EnhancedPasswordInput } from 'src/components/Login/LoginPasswordFormWrapper'; import { Subtitle } from '../components'; import { AppContext } from '../../../context/main'; @@ -21,10 +21,11 @@ export const SignInPassword = () => { }} > - setPassword(pswd)} + onUpdatePassword={(pswd: any) => setPassword(pswd)} error={error} autoFocus /> From 2eb695088f98260aef3f78711381d9cf936a1d8a Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Wed, 9 Apr 2025 16:14:11 +0200 Subject: [PATCH 2/2] linting and yarn - modify log screen --- .../Bonding/forms/nym-node/FormContext.tsx | 1 + .../src/components/Buy/SignMessageModal.tsx | 1 + .../components/CurrencyFormFieldWithPaste.tsx | 1 + nym-wallet/src/components/LogViewer/index.tsx | 180 ++++++++++++++++-- .../Login/LoginPasswordFormWrapper.tsx | 2 + .../Login/MnemonicLoginFormWrapper.tsx | 2 + .../src/components/TauriLinkWrapper.tsx | 1 - .../bonding/node-settings/node-test/index.tsx | 2 + 8 files changed, 169 insertions(+), 21 deletions(-) diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx index d3f95006f6..f110ecc02a 100644 --- a/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx +++ b/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx @@ -57,6 +57,7 @@ const FormContextProvider = ({ children }: { children: React.ReactNode }) => { const [signature, setSignature] = useState(''); const onError = (e: string) => { + // eslint-disable-next-line no-console console.error(e); }; diff --git a/nym-wallet/src/components/Buy/SignMessageModal.tsx b/nym-wallet/src/components/Buy/SignMessageModal.tsx index 23d3c9a634..bc9f6eebfc 100644 --- a/nym-wallet/src/components/Buy/SignMessageModal.tsx +++ b/nym-wallet/src/components/Buy/SignMessageModal.tsx @@ -21,6 +21,7 @@ export const SignMessageModal = ({ onClose }: { onClose: () => void }) => { const sig = await signMessage(message); setSignature(sig || ''); } catch (err) { + // eslint-disable-next-line no-console console.error('Signing failed:', err); setSignature(''); } diff --git a/nym-wallet/src/components/CurrencyFormFieldWithPaste.tsx b/nym-wallet/src/components/CurrencyFormFieldWithPaste.tsx index 5b636c8679..d21dfc27b9 100644 --- a/nym-wallet/src/components/CurrencyFormFieldWithPaste.tsx +++ b/nym-wallet/src/components/CurrencyFormFieldWithPaste.tsx @@ -105,6 +105,7 @@ export const CurrencyFormFieldWithPaste = ({ processPastedText(clipboardText); } } catch (err) { + // eslint-disable-next-line no-console console.error('Error accessing clipboard:', err); } } diff --git a/nym-wallet/src/components/LogViewer/index.tsx b/nym-wallet/src/components/LogViewer/index.tsx index c7d4e69911..b796715324 100644 --- a/nym-wallet/src/components/LogViewer/index.tsx +++ b/nym-wallet/src/components/LogViewer/index.tsx @@ -1,7 +1,18 @@ import React, { FC, useEffect, useRef, useState } from 'react'; import type { UnlistenFn } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event'; -import { Box, Paper, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; +import { + Box, + Paper, + Chip, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + useTheme, +} from '@mui/material'; // see https://github.com/tauri-apps/tauri-plugin-log/blob/dev/webview-src/index.ts#L4 enum LogLevel { @@ -29,24 +40,75 @@ const getLogLevelName = (value: LogLevel) => { } }; +const getLogLevelColor = (level: LogLevel, theme: any) => { + switch (level) { + case LogLevel.Trace: + return { + bg: '#e8f4f8', + color: '#2c3e50', + chipBg: '#e8f4f8', + }; + case LogLevel.Debug: + return { + bg: '#e8f0f8', + color: '#2c3e50', + chipBg: '#e8f0f8', + }; + case LogLevel.Info: + return { + bg: '#e8f8e8', + color: '#2c3e50', + chipBg: '#e8f8e8', + }; + case LogLevel.Warn: + return { + bg: '#fff8e0', + color: '#5d4037', + chipBg: '#fff8e0', + }; + case LogLevel.Error: + return { + bg: '#ffe8e8', + color: '#c0392b', + chipBg: '#ffe8e8', + }; + default: + return { + bg: theme.palette.mode === 'dark' ? '#333' : '#f0f0f0', + color: theme.palette.mode === 'dark' ? '#fff' : '#000', + chipBg: theme.palette.mode === 'dark' ? '#444' : '#e0e0e0', + }; + } +}; + // see https://github.com/tauri-apps/tauri-plugin-log/blob/dev/webview-src/index.ts#L147 interface RecordPayload { level: LogLevel; message: string; + timestamp?: number; // Adding timestamp for unique key generation } export const LogViewer: FC = () => { + const theme = useTheme(); const unlisten = useRef(); - const messages = useRef([]); + const [messages, setMessages] = useState([]); const [messageCount, setMessageCount] = useState(0); + const tableRef = useRef(null); useEffect(() => { listen('log://log', (event) => { - // eslint-disable-next-line no-console - console.log(event.payload); const payload = event.payload as RecordPayload; - messages.current.unshift(payload); + const payloadWithTimestamp = { + ...payload, + timestamp: Date.now(), + }; + + setMessages((prev) => [payloadWithTimestamp, ...prev]); setMessageCount((prev) => prev + 1); + + if (tableRef.current) { + tableRef.current.scrollTop = 0; + } }).then((fn) => { unlisten.current = fn; }); @@ -59,25 +121,102 @@ export const LogViewer: FC = () => { }, []); return ( - + - + - Severity - Log message + + Severity + + + Log message + - {messages.current.map((m) => ( - - - - - {m.message} - - ))} + {messages.map((m, index) => { + const levelColors = getLogLevelColor(m.level, theme); + return ( + + + + + + {m.message} + + + ); + })}
@@ -86,9 +225,10 @@ export const LogViewer: FC = () => { sx={{ p: 1, textAlign: 'right', - fontSize: 'small', - borderTop: '2px solid', - borderTopColor: (theme) => theme.palette.divider, + fontSize: '0.75rem', + borderTop: '1px solid #e0e0e0', + bgcolor: '#ffffff', + color: '#666666', }} > {messageCount} log entries since opening this window diff --git a/nym-wallet/src/components/Login/LoginPasswordFormWrapper.tsx b/nym-wallet/src/components/Login/LoginPasswordFormWrapper.tsx index 208df3f631..0217b81974 100644 --- a/nym-wallet/src/components/Login/LoginPasswordFormWrapper.tsx +++ b/nym-wallet/src/components/Login/LoginPasswordFormWrapper.tsx @@ -61,6 +61,7 @@ export const EnhancedPasswordInput: React.FC = ({ onUpdatePassword(clipboardText); } } catch (err) { + // eslint-disable-next-line no-console console.error('Failed to paste text:', err); } } @@ -94,6 +95,7 @@ export const EnhancedPasswordInput: React.FC = ({ } } } catch (err) { + // eslint-disable-next-line no-console console.error('Failed to paste from clipboard:', err); } }; diff --git a/nym-wallet/src/components/Login/MnemonicLoginFormWrapper.tsx b/nym-wallet/src/components/Login/MnemonicLoginFormWrapper.tsx index 30dcd105b4..07b9f17b26 100644 --- a/nym-wallet/src/components/Login/MnemonicLoginFormWrapper.tsx +++ b/nym-wallet/src/components/Login/MnemonicLoginFormWrapper.tsx @@ -67,6 +67,7 @@ export const EnhancedMnemonicInput: React.FC = ({ onUpdateMnemonic(clipboardText.trim()); } } catch (err) { + // eslint-disable-next-line no-console console.error('Failed to paste text:', err); } } @@ -101,6 +102,7 @@ export const EnhancedMnemonicInput: React.FC = ({ } } } catch (err) { + // eslint-disable-next-line no-console console.error('Failed to paste from clipboard:', err); } }; diff --git a/nym-wallet/src/components/TauriLinkWrapper.tsx b/nym-wallet/src/components/TauriLinkWrapper.tsx index f3632a1557..edf2e20839 100644 --- a/nym-wallet/src/components/TauriLinkWrapper.tsx +++ b/nym-wallet/src/components/TauriLinkWrapper.tsx @@ -12,7 +12,6 @@ export const TauriLink: React.FC = (props) => { if (href && (href.startsWith('http://') || href.startsWith('https://'))) { event.preventDefault(); - console.log('Opening link in browser:', href); await openUrl(href); } }; diff --git a/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx b/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx index 2e1dc533f8..ec4b4b1a89 100644 --- a/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx @@ -52,6 +52,7 @@ export const NodeTestPage = () => { } catch (e) { setError('Node test failed, please try again'); testStateRef.current = 'Stopped'; + // eslint-disable-next-line no-console console.log(e); } finally { setIsLoading(false); @@ -67,6 +68,7 @@ export const NodeTestPage = () => { await client.tester.init(validator, nodeTesterId); setNodeTestClient(client); } catch (e) { + // eslint-disable-next-line no-console console.log(e); setError('Failed to load node tester client, please try again'); }