diff --git a/src/renderer/inbox/App.tsx b/src/renderer/inbox/App.tsx index 2fb772e..b9b8719 100644 --- a/src/renderer/inbox/App.tsx +++ b/src/renderer/inbox/App.tsx @@ -11,6 +11,7 @@ import { RecoveryToast } from './components/RecoveryToast.js'; import { TagUndoToast } from './components/TagUndoToast.js'; import { ExpiryBanner } from './components/ExpiryBanner.js'; import { FailedBanner } from './components/FailedBanner.js'; +import { RecallBanner } from './components/RecallBanner.js'; export function App(): React.ReactElement { const { @@ -86,6 +87,7 @@ export function App(): React.ReactElement { + {tagFilter !== null && (
+
{formatted}
{!isTrash && showIntentBanner && ( diff --git a/src/renderer/inbox/components/RecallBanner.tsx b/src/renderer/inbox/components/RecallBanner.tsx new file mode 100644 index 0000000..231d5e3 --- /dev/null +++ b/src/renderer/inbox/components/RecallBanner.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useState } from 'react'; +import { useInbox } from '../store.js'; +import { inboxApi } from '../api.js'; + +export function RecallBanner(): React.ReactElement | null { + const candidate = useInbox((s) => s.recallCandidate); + const snoozeUntilMs = useInbox((s) => s.recallSnoozeUntilMs); + const shownIds = useInbox((s) => s.recallShownIds); + const openRecall = useInbox((s) => s.openRecall); + const dismissRecallNote = useInbox((s) => s.dismissRecallNote); + const snoozeRecall = useInbox((s) => s.snoozeRecall); + + // ExpiryBanner 패턴 — snoozeUntilMs 만료 시 force re-render + const [, setTick] = useState(0); + useEffect(() => { + if (snoozeUntilMs === null) return; + const remaining = snoozeUntilMs - Date.now(); + if (remaining <= 0) return; + const t = setTimeout(() => setTick((n) => n + 1), remaining); + return () => clearTimeout(t); + }, [snoozeUntilMs]); + + // first-render emit recall_shown (per-session 1회 per note) + useEffect(() => { + if (!candidate) return; + if (snoozeUntilMs !== null && Date.now() < snoozeUntilMs) return; + if (shownIds.has(candidate.id)) return; + void inboxApi.emitRecallShown(candidate.id); + useInbox.setState({ recallShownIds: new Set([...shownIds, candidate.id]) }); + }, [candidate, snoozeUntilMs, shownIds]); + + if (candidate === null) return null; + if (snoozeUntilMs !== null && Date.now() < snoozeUntilMs) return null; + + const ageDays = computeAgeDays(candidate.lastRecalledAt ?? candidate.createdAt); + const title = candidate.aiTitle ?? candidate.rawText.slice(0, 60); + + function onOpen() { + void openRecall(candidate!.id); + const el = document.getElementById(`note-${candidate!.id}`); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + + return ( +
+
+ 💭 오늘 회상해볼 노트 + + {title} + + {ageDays}일 전 +
+
+ + + +
+
+ ); +} + +function computeAgeDays(refIso: string): number { + const refMs = new Date(refIso).getTime(); + return Math.max(0, Math.floor((Date.now() - refMs) / 86_400_000)); +}