✨ feat(client): 弹窗可设自动关闭时间,完成提醒描述只显示标题

This commit is contained in:
pyh
2026-10-05 00:29:36 +08:00
parent 9218521dcb
commit faa3930ae8
12 changed files with 369 additions and 102 deletions
+40 -3
View File
@@ -1,6 +1,11 @@
/**
* The light in-app popup stack: its lifetime, its ordering, and its bound.
*
* The lifetime is the user's, read from the store at the moment a card is armed:
* the settings page offers a few delays plus "never", and re-arming after a hover
* picks the current choice up. `0` means the card waits for the user instead of a
* timer.
*
* @module @dsh-plugin/session-notify/client/core/toasts
*/
@@ -13,10 +18,19 @@ import { TOAST_DURATION_MS, TOAST_LIMIT } from '../constants.js'
*/
export function createToastStack({ store, copy }) {
const timers = new Map()
/** Cards the pointer is resting on: they carry no timer until it leaves. */
const held = new Set()
let seq = 0
/** The configured lifetime, falling back to the default for a value this version cannot use. */
const lifetime = () => {
const configured = store.getSnapshot().dismissAfterMs
return typeof configured === 'number' && configured >= 0 ? configured : TOAST_DURATION_MS
}
/** Drop one popup and cancel its lifetime timer. */
const closeToast = (id) => {
held.delete(id)
const timer = timers.get(id)
if (timer !== undefined) {
clearTimeout(timer)
@@ -27,21 +41,42 @@ export function createToastStack({ store, copy }) {
store.set({ ...snapshot, toasts: snapshot.toasts.filter((toast) => toast.id !== id) })
}
/** Arm one popup's lifetime; hovering calls `hold` first, then this again. */
/**
* Arm one popup's lifetime; hovering calls `hold` first, then this again.
* A lifetime of `0` arms nothing: the card stays until it is dismissed.
*/
const armToast = (id) => {
held.delete(id)
const timer = timers.get(id)
if (timer !== undefined) clearTimeout(timer)
timers.set(id, setTimeout(() => closeToast(id), TOAST_DURATION_MS))
timers.delete(id)
const duration = lifetime()
if (duration === 0) return
timers.set(id, setTimeout(() => closeToast(id), duration))
}
/** Pause one popup's lifetime while the pointer rests on it. */
const holdToast = (id) => {
held.add(id)
const timer = timers.get(id)
if (timer === undefined) return
clearTimeout(timer)
timers.delete(id)
}
/**
* Apply the current lifetime to the cards already on screen.
*
* Changing the setting restarts the countdown for what is visible — otherwise
* a card raised under the old lifetime would still close on the old schedule.
* A held card is left alone: the pointer is on it, so nothing may close it.
*/
const rearmAll = () => {
for (const toast of store.getSnapshot().toasts) {
if (!held.has(toast.id)) armToast(toast.id)
}
}
/**
* Show one light in-app popup: the trigger as the title, the conversation
* and its detail as the description, so the heading says what happened and
@@ -65,6 +100,7 @@ export function createToastStack({ store, copy }) {
clearTimeout(timer)
timers.delete(dropped.id)
}
held.delete(dropped.id)
}
store.set({ ...snapshot, toasts: next.slice(0, TOAST_LIMIT) })
armToast(toast.id)
@@ -74,7 +110,8 @@ export function createToastStack({ store, copy }) {
const dispose = () => {
for (const timer of timers.values()) clearTimeout(timer)
timers.clear()
held.clear()
}
return { showToast, closeToast, armToast, holdToast, dispose }
return { showToast, closeToast, armToast, holdToast, rearmAll, dispose }
}