* Service worker with toast notifications * Update CSP to allow fetches from now.sh * Fixed clearing timers * rounded icon for pwa (#1301) * rounded icon for pwa * cirle pwa app icon * fix fonts caching * fix app * fix css import * Updated csp tp inlcude worker-src: self * add worker CSP rule * use square icon Co-authored-by: Timur Khazamov <t1mmaas@skbkontur.ru> Co-authored-by: Faustino Kialungila <Faustino.kialungila@gmail.com> Co-authored-by: kbariotis <konmpar@gmail.com>
98 lines
2.4 KiB
TypeScript
98 lines
2.4 KiB
TypeScript
import React from "react";
|
|
import { render } from "react-dom";
|
|
import Stack from "./Stack";
|
|
import { Island } from "./Island";
|
|
import "./Toast.css";
|
|
import { close } from "./icons";
|
|
|
|
const TOAST_TIMEOUT = 7000;
|
|
|
|
function ToastRenderer(props: {
|
|
toasts: Map<number, React.ReactNode>;
|
|
onCloseRequest: (id: number) => void;
|
|
}) {
|
|
return (
|
|
<Stack.Col gap={2} align="center">
|
|
{[...props.toasts.entries()].map(([id, toast]) => (
|
|
<Island key={id} padding={3}>
|
|
<div className="Toast">
|
|
<div className="Toast__content">{toast}</div>
|
|
<button
|
|
className="Toast__close"
|
|
onClick={() => props.onCloseRequest(id)}
|
|
>
|
|
{close}
|
|
</button>
|
|
</div>
|
|
</Island>
|
|
))}
|
|
</Stack.Col>
|
|
);
|
|
}
|
|
|
|
let toastsRootNode: HTMLDivElement;
|
|
function getToastsRootNode() {
|
|
return toastsRootNode || (toastsRootNode = initToastsRootNode());
|
|
}
|
|
|
|
function initToastsRootNode() {
|
|
const div = window.document.createElement("div");
|
|
document.body.appendChild(div);
|
|
div.className = "Toast__container";
|
|
return div;
|
|
}
|
|
|
|
function renderToasts(
|
|
toasts: Map<number, React.ReactNode>,
|
|
onCloseRequest: (id: number) => void,
|
|
) {
|
|
render(
|
|
<ToastRenderer toasts={toasts} onCloseRequest={onCloseRequest} />,
|
|
getToastsRootNode(),
|
|
);
|
|
}
|
|
|
|
let incrementalId = 0;
|
|
function getToastId() {
|
|
return incrementalId++;
|
|
}
|
|
|
|
class ToastManager {
|
|
private toasts = new Map<number, React.ReactNode>();
|
|
private timers = new Map<number, number>();
|
|
|
|
public push(message: React.ReactNode, shiftAfterMs: number) {
|
|
const id = getToastId();
|
|
this.toasts.set(id, message);
|
|
if (isFinite(shiftAfterMs)) {
|
|
const handle = window.setTimeout(() => this.pop(id), shiftAfterMs);
|
|
this.timers.set(id, handle);
|
|
}
|
|
this.render();
|
|
}
|
|
|
|
private pop = (id: number) => {
|
|
const handle = this.timers.get(id);
|
|
if (handle) {
|
|
window.clearTimeout(handle);
|
|
this.timers.delete(id);
|
|
}
|
|
this.toasts.delete(id);
|
|
this.render();
|
|
};
|
|
|
|
private render() {
|
|
renderToasts(this.toasts, this.pop);
|
|
}
|
|
}
|
|
|
|
let toastManagerInstance: ToastManager;
|
|
function getToastManager(): ToastManager {
|
|
return toastManagerInstance ?? (toastManagerInstance = new ToastManager());
|
|
}
|
|
|
|
export function push(message: React.ReactNode, manualClose = false) {
|
|
const toastManager = getToastManager();
|
|
toastManager.push(message, manualClose ? Infinity : TOAST_TIMEOUT);
|
|
}
|