fix: clear transient ui feedback timers (#3385)

This commit is contained in:
Neil
2026-05-29 17:13:15 -07:00
committed by GitHub
parent 191183882a
commit 2a08d68631
8 changed files with 79 additions and 15 deletions
+10 -2
View File
@@ -1,5 +1,5 @@
/* oxlint-disable max-lines */
import React, { useCallback, useDeferredValue, useMemo, useState } from 'react'
import React, { useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react'
import { AlertTriangle, Check, Copy } from 'lucide-react'
import { useAppStore } from '@/store'
import { useActiveWorktree } from '@/store/selectors'
@@ -66,6 +66,15 @@ function InstallRgGuidance({
guidance?: string | null
}): React.JSX.Element {
const [copied, setCopied] = useState(false)
useEffect(() => {
if (!copied) {
return
}
const timeout = window.setTimeout(() => setCopied(false), 1500)
return () => window.clearTimeout(timeout)
}, [copied])
const handleCopy = useCallback(() => {
if (!command) {
return
@@ -78,7 +87,6 @@ function InstallRgGuidance({
.writeClipboardText(command)
.then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
.catch(() => {
/* best-effort */
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { Copy, Check } from 'lucide-react'
type CodeBlockCopyButtonProps = React.HTMLAttributes<HTMLPreElement> & {
@@ -11,6 +11,14 @@ export default function CodeBlockCopyButton({
}: CodeBlockCopyButtonProps): React.JSX.Element {
const [copied, setCopied] = useState(false)
useEffect(() => {
if (!copied) {
return
}
const timeout = window.setTimeout(() => setCopied(false), 1500)
return () => window.clearTimeout(timeout)
}, [copied])
const handleCopy = useCallback(() => {
// Extract the text content from the nested <code> element rendered by
// react-markdown inside <pre>. We walk the React children tree to grab the
@@ -29,7 +37,6 @@ export default function CodeBlockCopyButton({
.writeClipboardText(text)
.then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
.catch(() => {
// Silently swallow clipboard write failures (e.g. permission denied).
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
import type { NodeViewProps } from '@tiptap/react'
import { Copy, Check } from 'lucide-react'
@@ -51,6 +51,14 @@ export function RichMarkdownCodeBlock({
const isMermaid = language === 'mermaid'
useEffect(() => {
if (!copied) {
return
}
const timeout = window.setTimeout(() => setCopied(false), 1500)
return () => window.clearTimeout(timeout)
}, [copied])
const onChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
updateAttributes({ language: e.target.value })
@@ -66,7 +74,6 @@ export function RichMarkdownCodeBlock({
.writeClipboardText(text)
.then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
.catch(() => {
// Silently swallow clipboard write failures (e.g. permission denied).
@@ -1,6 +1,6 @@
/* eslint-disable max-lines -- Why: co-locating all checks-panel sub-components (checks list,
conflict sections, threaded PR comments) keeps the shared icon/color maps in one place. */
import React, { useCallback, useState } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import {
CircleCheck,
CircleX,
@@ -357,12 +357,19 @@ export function ChecksList({
function CopyButton({ text }: { text: string }): React.JSX.Element {
const [copied, setCopied] = useState(false)
useEffect(() => {
if (!copied) {
return
}
const timeout = window.setTimeout(() => setCopied(false), 1500)
return () => window.clearTimeout(timeout)
}, [copied])
const handleCopy = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
void window.api.ui.writeClipboardText(text).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
},
[text]
@@ -390,12 +397,19 @@ function ResolveButton({
}): React.JSX.Element {
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!loading) {
return
}
const timeout = window.setTimeout(() => setLoading(false), 300)
return () => window.clearTimeout(timeout)
}, [loading])
const handleClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
setLoading(true)
onResolve(threadId, !isResolved)
setTimeout(() => setLoading(false), 300)
},
[threadId, isResolved, onResolve]
)
@@ -215,6 +215,14 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
void loadConfigs()
}, [loadConfigs])
useEffect(() => {
if (!createConfirm) {
return
}
const timeout = window.setTimeout(() => setCreateConfirm(false), 3000)
return () => window.clearTimeout(timeout)
}, [createConfirm])
const handleOpen = (config: LoadedMcpConfigInspection): void => {
setActiveWorktree(targetWorktreeId)
const targetGroupId = ensureWorktreeRootGroup(targetWorktreeId)
@@ -234,7 +242,6 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
const handleCreateStarter = async (): Promise<void> => {
if (!createConfirm) {
setCreateConfirm(true)
window.setTimeout(() => setCreateConfirm(false), 3000)
return
}
@@ -53,6 +53,14 @@ export function MobilePane(): React.JSX.Element {
const [refreshingNetworkInterfaces, setRefreshingNetworkInterfaces] = useState(false)
const [codeCopied, setCodeCopied] = useState(false)
useEffect(() => {
if (!codeCopied) {
return
}
const timeout = window.setTimeout(() => setCodeCopied(false), 2000)
return () => window.clearTimeout(timeout)
}, [codeCopied])
const loadDevices = useCallback(async () => {
try {
const result = await window.api.mobile.listDevices()
@@ -141,7 +149,6 @@ export function MobilePane(): React.JSX.Element {
// IPC clipboard which the rest of the app uses everywhere.
await window.api.ui.writeClipboardText(pairingUrl)
setCodeCopied(true)
setTimeout(() => setCodeCopied(false), 2000)
} catch {
toast.error('Failed to copy pairing code')
}
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import type { OrcaHooks, Repo, RepoHookSettings } from '../../../../shared/types'
import { getRepoKindLabel, isFolderRepo } from '../../../../shared/repo-kind'
import { Button } from '../ui/button'
@@ -59,6 +59,14 @@ export function RepositoryPane({
// request to hide every child row that does not repeat the project name.
const forceFullPaneForRepoMatch = matchesRepositoryIdentitySearch(searchQuery, repo)
useEffect(() => {
if (!copiedTemplate) {
return
}
const timeout = window.setTimeout(() => setCopiedTemplate(false), 1500)
return () => window.clearTimeout(timeout)
}, [copiedTemplate])
const handleRemoveProject = (repoId: string) => {
if (confirmingRemove === repoId) {
removeProject(repoId)
@@ -84,7 +92,6 @@ export function RepositoryPane({
archive: |
echo "Cleaning up before archive"`)
setCopiedTemplate(true)
window.setTimeout(() => setCopiedTemplate(false), 1500)
}
const allEntries = getRepositoryPaneSearchEntries(repo)
@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { toPng } from 'html-to-image'
import { Check, Copy, Share2 } from 'lucide-react'
import { Button } from '../ui/button'
@@ -21,6 +21,14 @@ export function ShareUsageButton(props: ShareUsageButtonProps): React.JSX.Elemen
const [copied, setCopied] = useState(false)
const [capturing, setCapturing] = useState(false)
useEffect(() => {
if (!copied) {
return
}
const timeout = window.setTimeout(() => setCopied(false), 2000)
return () => window.clearTimeout(timeout)
}, [copied])
const captureToClipboard = useCallback(async () => {
if (!cardRef.current || capturing) {
return
@@ -42,7 +50,6 @@ export function ShareUsageButton(props: ShareUsageButtonProps): React.JSX.Elemen
const ok = await captureToClipboard()
if (ok) {
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}, [captureToClipboard])