Skip to content

Commit 045b14f

Browse files
authored
Merge pull request #1069 from Mrwicks00/feat/1020-1030-1032-emoji-refocus-connection-watchlist
feat(frontend): emoji refocus, connection indicator, and wallet watch…
2 parents 20eb7b9 + 1210945 commit 045b14f

6 files changed

Lines changed: 546 additions & 2 deletions

File tree

Dechat/dex_with_fiat_frontend/src/components/ChatInput.tsx

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import React, { useEffect, useRef, useState } from 'react';
4-
import { Send, Loader2, AlertTriangle } from 'lucide-react';
4+
import { Send, Loader2, AlertTriangle, Smile } from 'lucide-react';
55
import { motion, AnimatePresence } from 'framer-motion';
66
import { useTranslation } from '@/contexts/TranslationContext';
77
import { useStellarWallet } from '@/contexts/StellarWalletContext';
@@ -40,6 +40,8 @@ interface ChatInputProps {
4040
* - Ctrl+Shift+C (Cmd+Shift+C on Mac): Cancel pending request
4141
*/
4242

43+
const COMMON_EMOJIS = ['😊', '😂', '🙏', '👍', '❤️', '🔥', '✅', '🚀', '💰', '📊', '⭐', '🎉'];
44+
4345
export default function ChatInput({
4446
onSendMessage,
4547
onCancelRequest,
@@ -76,8 +78,29 @@ export default function ChatInput({
7678
const [paletteIndex, setPaletteIndex] = useState(0);
7779

7880
const bottomRef = useRef<HTMLDivElement>(null);
81+
const textareaRef = useRef<HTMLTextAreaElement>(null);
82+
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
7983
const isMobile = useMediaQuery('(max-width: 639px)');
8084

85+
const insertEmoji = (emoji: string) => {
86+
const el = textareaRef.current;
87+
if (el) {
88+
const start = el.selectionStart ?? message.length;
89+
const end = el.selectionEnd ?? message.length;
90+
const next = message.slice(0, start) + emoji + message.slice(end);
91+
setMessage(next);
92+
// iOS Safari loses focus when the emoji picker opens; re-focus after state update
93+
setTimeout(() => {
94+
el.focus();
95+
const pos = start + emoji.length;
96+
el.setSelectionRange(pos, pos);
97+
}, 0);
98+
} else {
99+
setMessage((prev) => prev + emoji);
100+
}
101+
setShowEmojiPicker(false);
102+
};
103+
81104
const { execute: executeSubmit, isProcessing: isSubmitting } = useIdempotentAction({
82105
cooldownMs: 1000,
83106
logSuppressed: true,
@@ -237,6 +260,20 @@ export default function ChatInput({
237260
return () => window.removeEventListener('keydown', handler);
238261
}, [onNewChat, onOpenHistory, onOpenBridgeModal, onCancelRequest]);
239262

263+
// Close emoji picker on outside click
264+
useEffect(() => {
265+
if (!showEmojiPicker) return;
266+
const handler = (e: MouseEvent) => {
267+
const target = e.target as Node;
268+
const picker = document.querySelector('[aria-label="Emoji picker"]');
269+
if (picker && !picker.contains(target)) {
270+
setShowEmojiPicker(false);
271+
}
272+
};
273+
document.addEventListener('mousedown', handler);
274+
return () => document.removeEventListener('mousedown', handler);
275+
}, [showEmojiPicker]);
276+
240277
// Load draft when session changes
241278
useEffect(() => {
242279
if (sessionId) {
@@ -383,7 +420,27 @@ export default function ChatInput({
383420
className={isMobile ? 'flex flex-col gap-2' : 'flex items-end space-x-3'}
384421
>
385422
<div className="flex-1 relative min-w-0">
423+
{showEmojiPicker && (
424+
<div
425+
className="absolute bottom-full mb-2 left-0 z-50 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-2xl p-2 grid grid-cols-6 gap-1"
426+
role="dialog"
427+
aria-label="Emoji picker"
428+
>
429+
{COMMON_EMOJIS.map((emoji) => (
430+
<button
431+
key={emoji}
432+
type="button"
433+
onClick={() => insertEmoji(emoji)}
434+
className="text-xl p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
435+
aria-label={emoji}
436+
>
437+
{emoji}
438+
</button>
439+
))}
440+
</div>
441+
)}
386442
<textarea
443+
ref={textareaRef}
387444
data-testid="chat-input-textarea"
388445
value={message}
389446
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => handleInputChange(e.target.value)}
@@ -415,6 +472,17 @@ export default function ChatInput({
415472
/>
416473
</div>
417474

475+
<button
476+
type="button"
477+
onClick={() => setShowEmojiPicker((v) => !v)}
478+
title="Insert emoji"
479+
aria-label="Insert emoji"
480+
aria-expanded={showEmojiPicker}
481+
className={`flex items-center justify-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 rounded-lg transition-colors ${isMobile ? 'h-11 w-11' : 'w-10 h-10'}`}
482+
>
483+
<Smile className="w-5 h-5" />
484+
</button>
485+
418486
<button
419487
type="submit"
420488
data-testid="chat-input-send"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use client';
2+
3+
import { X, Wifi, WifiOff, AlertTriangle } from 'lucide-react';
4+
import { useTheme } from '@/contexts/ThemeContext';
5+
import { useStellarWallet, EXPECTED_NETWORK } from '@/contexts/StellarWalletContext';
6+
7+
interface NetworkStatusModalProps {
8+
isOpen: boolean;
9+
onClose: () => void;
10+
}
11+
12+
export default function NetworkStatusModal({ isOpen, onClose }: NetworkStatusModalProps) {
13+
const { isDarkMode } = useTheme();
14+
const { connection, isNetworkMismatch } = useStellarWallet();
15+
16+
if (!isOpen) return null;
17+
18+
const status = !connection.isConnected
19+
? 'disconnected'
20+
: isNetworkMismatch
21+
? 'mismatch'
22+
: 'connected';
23+
24+
const statusConfig = {
25+
connected: {
26+
dot: 'bg-green-400',
27+
label: 'Connected',
28+
description: `Wallet is connected to the Stellar ${connection.network} network.`,
29+
Icon: Wifi,
30+
iconClass: isDarkMode ? 'text-green-400' : 'text-green-600',
31+
},
32+
mismatch: {
33+
dot: 'bg-amber-400',
34+
label: 'Network Mismatch',
35+
description: `Wallet is connected to ${connection.network} but the app expects ${EXPECTED_NETWORK}. Transactions are disabled.`,
36+
Icon: AlertTriangle,
37+
iconClass: isDarkMode ? 'text-amber-400' : 'text-amber-600',
38+
},
39+
disconnected: {
40+
dot: 'bg-red-500',
41+
label: 'Disconnected',
42+
description: 'No Stellar wallet is connected. Connect Freighter to send transactions.',
43+
Icon: WifiOff,
44+
iconClass: isDarkMode ? 'text-red-400' : 'text-red-600',
45+
},
46+
}[status];
47+
48+
return (
49+
<>
50+
<div
51+
className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"
52+
onClick={onClose}
53+
aria-hidden="true"
54+
/>
55+
<div
56+
role="dialog"
57+
aria-modal="true"
58+
aria-label="Network status"
59+
className={`fixed z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 rounded-xl shadow-2xl border p-6 transition-colors duration-300 ${
60+
isDarkMode
61+
? 'bg-gray-900 border-gray-700 text-gray-100'
62+
: 'bg-white border-gray-200 text-gray-900'
63+
}`}
64+
>
65+
<div className="flex items-center justify-between mb-4">
66+
<h2 className="text-base font-semibold">Network Status</h2>
67+
<button
68+
onClick={onClose}
69+
aria-label="Close"
70+
className={`p-1.5 rounded-lg transition-colors ${
71+
isDarkMode
72+
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-800'
73+
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
74+
}`}
75+
>
76+
<X className="w-4 h-4" />
77+
</button>
78+
</div>
79+
80+
<div className="flex items-start gap-3">
81+
<span className={`mt-1 w-3 h-3 rounded-full flex-shrink-0 ${statusConfig.dot}`} />
82+
<div>
83+
<p className="font-medium text-sm">{statusConfig.label}</p>
84+
<p className={`text-xs mt-1 ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
85+
{statusConfig.description}
86+
</p>
87+
</div>
88+
</div>
89+
90+
{connection.isConnected && (
91+
<dl className={`mt-4 space-y-2 text-xs border-t pt-4 ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}>
92+
<div className="flex justify-between">
93+
<dt className={isDarkMode ? 'text-gray-400' : 'text-gray-500'}>Address</dt>
94+
<dd className="font-mono">
95+
{connection.address.slice(0, 6)}{connection.address.slice(-4)}
96+
</dd>
97+
</div>
98+
<div className="flex justify-between">
99+
<dt className={isDarkMode ? 'text-gray-400' : 'text-gray-500'}>Network</dt>
100+
<dd>{connection.network || '—'}</dd>
101+
</div>
102+
<div className="flex justify-between">
103+
<dt className={isDarkMode ? 'text-gray-400' : 'text-gray-500'}>Expected</dt>
104+
<dd>{EXPECTED_NETWORK}</dd>
105+
</div>
106+
</dl>
107+
)}
108+
</div>
109+
</>
110+
);
111+
}

Dechat/dex_with_fiat_frontend/src/components/StellarChatInterface.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import ChatHistorySidebar from './ChatHistorySidebar';
4343
import ChatInput from './ChatInput';
4444
import ChatMessages from './ChatMessages';
4545
import ErrorBoundary from './ErrorBoundary';
46+
import NetworkStatusModal from './NetworkStatusModal';
4647
import NotificationsCenter from './NotificationsCenter';
4748
import StellarFiatModal from './StellarFiatModal';
4849
import UserSettings from './UserSettings';
@@ -52,6 +53,8 @@ import ReceiptDrawer from './ReceiptDrawerWrapper';
5253
import { useTxHistory } from '@/hooks/useTxHistory';
5354
import { useChatHistory } from '@/hooks/useChatHistory';
5455
import { useSplitView } from '@/hooks/useSplitView';
56+
import { useWatchlist } from '@/hooks/useWatchlist';
57+
import { useWatchedWalletNotifications } from '@/hooks/useWatchedWalletNotifications';
5558
import { subscribeToQueue, processQueue } from '@/lib/networkQueue';
5659
import CopyButton from '@/components/ui/CopyButton';
5760
import SplitViewComparison from './SplitViewComparison';
@@ -108,6 +111,12 @@ function StellarChatInterfaceContent() {
108111
>(null);
109112
const [isReceiptDrawerOpen, setIsReceiptDrawerOpen] = useState(false);
110113
const [showSearch, setShowSearch] = useState(false);
114+
const [showNetworkStatusModal, setShowNetworkStatusModal] = useState(false);
115+
116+
// Watched wallet notifications (issue #1032)
117+
const { watchlist } = useWatchlist();
118+
useWatchedWalletNotifications(watchlist);
119+
111120
// Show the chat skeleton until the client has hydrated so the first paint
112121
// isn't an empty conversation pane before messages are restored.
113122
const [isHydrated, setIsHydrated] = useState(false);
@@ -549,6 +558,37 @@ function StellarChatInterfaceContent() {
549558
{healthBadge.label}
550559
</div>
551560
{/* ─────────────────────────────────────────────────────────────── */}
561+
562+
{/* ── Stellar connection status indicator (issue #1030) ─────────── */}
563+
{(() => {
564+
const connDot = !connection.isConnected
565+
? 'bg-red-500'
566+
: isNetworkMismatch
567+
? 'bg-amber-400'
568+
: 'bg-green-400';
569+
const connTitle = !connection.isConnected
570+
? 'Stellar: Disconnected'
571+
: isNetworkMismatch
572+
? `Stellar: Network mismatch (${connection.network})`
573+
: `Stellar: Connected (${connection.network})`;
574+
return (
575+
<button
576+
type="button"
577+
title={connTitle}
578+
aria-label={connTitle}
579+
onClick={() => setShowNetworkStatusModal(true)}
580+
className={`hidden sm:flex items-center gap-1.5 px-2 py-1 rounded-full text-[11px] font-medium transition-colors ${
581+
isDarkMode
582+
? 'bg-gray-800 text-gray-300 hover:bg-gray-700'
583+
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
584+
}`}
585+
>
586+
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${connDot}`} />
587+
Stellar
588+
</button>
589+
);
590+
})()}
591+
{/* ─────────────────────────────────────────────────────────────── */}
552592
</div>
553593

554594
<div className="flex items-center gap-2">
@@ -1016,6 +1056,12 @@ function StellarChatInterfaceContent() {
10161056
onClose={() => setShowSettings(false)}
10171057
/>
10181058

1059+
{/* Network status modal (issue #1030) */}
1060+
<NetworkStatusModal
1061+
isOpen={showNetworkStatusModal}
1062+
onClose={() => setShowNetworkStatusModal(false)}
1063+
/>
1064+
10191065
{/* Receipt Drawer */}
10201066
<ReceiptDrawer
10211067
isOpen={isReceiptDrawerOpen}

0 commit comments

Comments
 (0)