118 lines
3.8 KiB
JavaScript
118 lines
3.8 KiB
JavaScript
/**
|
|
* 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
|
|
*/
|
|
|
|
import { TOAST_DURATION_MS, TOAST_LIMIT } from '../constants.js'
|
|
|
|
/**
|
|
* Build the popup stack over one store.
|
|
* @param deps - the plugin store, and the copy table a candidate is rendered with.
|
|
* @returns the stack's operations, including the three the popup element calls back into.
|
|
*/
|
|
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)
|
|
timers.delete(id)
|
|
}
|
|
const snapshot = store.getSnapshot()
|
|
if (!snapshot.toasts.some((toast) => toast.id === id)) return
|
|
store.set({ ...snapshot, toasts: snapshot.toasts.filter((toast) => toast.id !== id) })
|
|
}
|
|
|
|
/**
|
|
* 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.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
|
|
* the line under it says where.
|
|
*/
|
|
const showToast = (candidate) => {
|
|
const notice = copy.copyFor(candidate)
|
|
seq += 1
|
|
const toast = {
|
|
id: `dsn-${String(seq)}`,
|
|
kind: notice.kind,
|
|
title: notice.title,
|
|
body: notice.body,
|
|
sessionId: candidate.sessionId,
|
|
}
|
|
const snapshot = store.getSnapshot()
|
|
const next = [toast, ...snapshot.toasts]
|
|
for (const dropped of next.slice(TOAST_LIMIT)) {
|
|
const timer = timers.get(dropped.id)
|
|
if (timer !== undefined) {
|
|
clearTimeout(timer)
|
|
timers.delete(dropped.id)
|
|
}
|
|
held.delete(dropped.id)
|
|
}
|
|
store.set({ ...snapshot, toasts: next.slice(0, TOAST_LIMIT) })
|
|
armToast(toast.id)
|
|
}
|
|
|
|
/** Cancel every lifetime timer: the plugin is being disposed, so nothing may fire later. */
|
|
const dispose = () => {
|
|
for (const timer of timers.values()) clearTimeout(timer)
|
|
timers.clear()
|
|
held.clear()
|
|
}
|
|
|
|
return { showToast, closeToast, armToast, holdToast, rearmAll, dispose }
|
|
}
|