|
48 | 48 | let emojiOpen = $state<boolean>(false); |
49 | 49 | let threadMenuOpen = $state<boolean>(false); |
50 | 50 | let clearing = $state<boolean>(false); |
| 51 | + // Inline confirm step. The Tauri webview silently returned `false` |
| 52 | + // from `window.confirm`, so the click chain never reached the actual |
| 53 | + // clear path. Use UI state for a two-click flow that works in every |
| 54 | + // webview. |
| 55 | + let confirmingClear = $state<boolean>(false); |
| 56 | + let confirmClearBtn: HTMLButtonElement | undefined = $state(); |
51 | 57 |
|
52 | 58 | // Fast-poll cadence while a thread is focused. SDK pulls are global, |
53 | 59 | // so this just shortens latency for whichever chat the user is |
|
70 | 76 |
|
71 | 77 | function toggleThreadMenu() { |
72 | 78 | threadMenuOpen = !threadMenuOpen; |
| 79 | + // Reset the confirm step whenever the menu opens, so it always |
| 80 | + // starts at "Clear chat" not the half-armed "Confirm" view. |
| 81 | + if (threadMenuOpen) confirmingClear = false; |
73 | 82 | } |
74 | 83 |
|
75 | | - async function clearChat() { |
| 84 | + function requestClearChat() { |
| 85 | + confirmingClear = true; |
| 86 | + // Move focus onto the destructive button so the next Enter / Space |
| 87 | + // confirms (or Escape via the menu cancels). Without this, focus |
| 88 | + // stays on the now-removed "Clear chat" button and falls back to |
| 89 | + // the document. |
| 90 | + tick().then(() => confirmClearBtn?.focus()); |
| 91 | + } |
| 92 | +
|
| 93 | + function cancelClearChat() { |
| 94 | + confirmingClear = false; |
| 95 | + } |
| 96 | +
|
| 97 | + async function confirmClearChat() { |
| 98 | + confirmingClear = false; |
76 | 99 | threadMenuOpen = false; |
77 | 100 | if (!activeConversation) return; |
78 | | - const label = activeConversation.label; |
79 | | - if ( |
80 | | - typeof window !== "undefined" && |
81 | | - !window.confirm(`Clear all messages with ${label}?`) |
82 | | - ) { |
83 | | - return; |
84 | | - } |
85 | 101 | clearing = true; |
86 | 102 | try { |
87 | 103 | const incomingIds = activeConversation.messages |
|
214 | 230 | activeKey = key; |
215 | 231 | replyTo = null; |
216 | 232 | sendError = ""; |
| 233 | + // Don't carry per-thread overflow state across a switch — a |
| 234 | + // half-armed "Yes, clear" from chat A would otherwise act on |
| 235 | + // chat B once it became active. |
| 236 | + threadMenuOpen = false; |
| 237 | + confirmingClear = false; |
217 | 238 | await tick(); |
218 | 239 | if (composerEl) composerEl.focus(); |
219 | 240 | // Mark-read is handled by the $effect below so messages that |
|
234 | 255 | } |
235 | 256 | }); |
236 | 257 |
|
| 258 | + // Identity switch / lock from anywhere (header pill, /identities |
| 259 | + // page) drops the transient per-thread menu state. Without this, a |
| 260 | + // half-armed "Yes, clear" arming under identity A could carry into |
| 261 | + // identity B and act on B's data if the new identity happens to |
| 262 | + // share the same activeKey (same contact username). |
| 263 | + $effect(() => { |
| 264 | + void $activeIdentity; |
| 265 | + threadMenuOpen = false; |
| 266 | + confirmingClear = false; |
| 267 | + }); |
| 268 | +
|
237 | 269 | function closeConversation() { |
238 | 270 | activeKey = null; |
239 | 271 | replyTo = null; |
| 272 | + threadMenuOpen = false; |
| 273 | + confirmingClear = false; |
240 | 274 | } |
241 | 275 |
|
242 | 276 | async function send() { |
|
535 | 569 | >⋯</button> |
536 | 570 | {#if threadMenuOpen} |
537 | 571 | <div class="thread-menu" role="menu"> |
538 | | - <button |
539 | | - type="button" |
540 | | - class="thread-menu-item danger-text" |
541 | | - onclick={clearChat} |
542 | | - disabled={clearing || activeConversation.messages.length === 0} |
543 | | - > |
544 | | - {clearing ? "Clearing…" : "Clear chat"} |
545 | | - </button> |
| 572 | + {#if !confirmingClear} |
| 573 | + <button |
| 574 | + type="button" |
| 575 | + class="thread-menu-item danger-text" |
| 576 | + onclick={requestClearChat} |
| 577 | + disabled={clearing || activeConversation.messages.length === 0} |
| 578 | + > |
| 579 | + {clearing ? "Clearing…" : "Clear chat"} |
| 580 | + </button> |
| 581 | + {:else} |
| 582 | + <p class="thread-menu-prompt">Clear all messages with this contact?</p> |
| 583 | + <button |
| 584 | + bind:this={confirmClearBtn} |
| 585 | + type="button" |
| 586 | + class="thread-menu-item danger-text" |
| 587 | + onclick={confirmClearChat} |
| 588 | + disabled={clearing} |
| 589 | + > |
| 590 | + {clearing ? "Clearing…" : "Yes, clear"} |
| 591 | + </button> |
| 592 | + <button |
| 593 | + type="button" |
| 594 | + class="thread-menu-item" |
| 595 | + onclick={cancelClearChat} |
| 596 | + disabled={clearing} |
| 597 | + > |
| 598 | + Cancel |
| 599 | + </button> |
| 600 | + {/if} |
546 | 601 | </div> |
547 | 602 | {/if} |
548 | 603 | </div> |
|
900 | 955 | padding: 0.5em 0.7em; |
901 | 956 | border-radius: 6px; |
902 | 957 | } |
| 958 | + .thread-menu-prompt { |
| 959 | + margin: 0 0 0.4rem; |
| 960 | + padding: 0.4em 0.7em 0; |
| 961 | + font-size: 12px; |
| 962 | + color: var(--muted-strong); |
| 963 | + } |
903 | 964 | .thread-menu-item:hover:not(:disabled) { |
904 | 965 | background: var(--accent-softer); |
905 | 966 | border-color: var(--accent); |
|
0 commit comments