diff --git a/apps/client/package.json b/apps/client/package.json
index 91eaad1bc..b2d084008 100644
--- a/apps/client/package.json
+++ b/apps/client/package.json
@@ -27,6 +27,7 @@
"@mermaid-js/layout-elk": "0.2.0",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
+ "@preact/signals": "2.5.1",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
diff --git a/apps/client/src/layouts/layout_commons.tsx b/apps/client/src/layouts/layout_commons.tsx
index 62f810430..3620d495d 100644
--- a/apps/client/src/layouts/layout_commons.tsx
+++ b/apps/client/src/layouts/layout_commons.tsx
@@ -24,6 +24,7 @@ import InfoDialog from "../widgets/dialogs/info.js";
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx";
+import ToastContainer from "../widgets/Toast.jsx";
export function applyModals(rootContainer: RootContainer) {
rootContainer
@@ -50,5 +51,6 @@ export function applyModals(rootContainer: RootContainer) {
.child()
.child()
.child()
- .child();
+ .child()
+ .child()
}
diff --git a/apps/client/src/services/toast.ts b/apps/client/src/services/toast.ts
index 8c55efeee..c99353821 100644
--- a/apps/client/src/services/toast.ts
+++ b/apps/client/src/services/toast.ts
@@ -1,3 +1,5 @@
+import { signal } from "@preact/signals";
+
import utils from "./utils.js";
export interface ToastOptions {
@@ -11,83 +13,28 @@ export interface ToastOptions {
progress?: number;
}
-function toast({ title, icon, message, id, delay, autohide, progress }: ToastOptions) {
- const $toast = $(title
- ? `\
-
`
- : `
- `
- );
+export type ToastOptionsWithRequiredId = Omit & Required>;
- $toast.toggleClass("no-title", !title);
- $toast.find(".toast-title").text(title ?? "");
- $toast.find(".toast-body").html(message);
- $toast.find(".toast-progress").css("width", `${(progress ?? 0) * 100}%`);
-
- if (id) {
- $toast.attr("id", `toast-${id}`);
- }
-
- $("#toast-container").append($toast);
-
- $toast.toast({
- delay: delay || 3000,
- autohide: !!autohide
- });
-
- $toast.on("hidden.bs.toast", (e) => e.target.remove());
-
- $toast.toast("show");
-
- return $toast;
-}
-
-function showPersistent(options: ToastOptions) {
- let $toast = $(`#toast-${options.id}`);
-
- if ($toast.length > 0) {
- $toast.find(".toast-body").html(options.message);
- $toast.find(".toast-progress").css("width", `${(options.progress ?? 0) * 100}%`);
+function showPersistent(options: ToastOptionsWithRequiredId) {
+ const existingToast = toasts.value.find(toast => toast.id === options.id);
+ if (existingToast) {
+ updateToast(options.id, options);
} else {
options.autohide = false;
-
- $toast = toast(options);
- }
-
- if (options.closeAfter) {
- setTimeout(() => $toast.remove(), options.closeAfter);
+ addToast(options);
}
}
function closePersistent(id: string) {
- $(`#toast-${id}`).remove();
+ removeToastFromStore(id);
}
function showMessage(message: string, delay = 2000, icon = "check") {
console.debug(utils.now(), "message:", message);
- toast({
+ addToast({
icon,
- message: message,
+ message,
autohide: true,
delay
});
@@ -96,26 +43,50 @@ function showMessage(message: string, delay = 2000, icon = "check") {
export function showError(message: string, delay = 10000) {
console.log(utils.now(), "error: ", message);
- toast({
+ addToast({
icon: "bx bx-error-circle",
- message: message,
+ message,
autohide: true,
delay
- });
+ })
}
function showErrorTitleAndMessage(title: string, message: string, delay = 10000) {
console.log(utils.now(), "error: ", message);
- toast({
- title: title,
+ addToast({
+ title,
icon: "bx bx-error-circle",
- message: message,
+ message,
autohide: true,
delay
});
}
+//#region Toast store
+export const toasts = signal([]);
+
+function addToast(opts: ToastOptions) {
+ const id = opts.id ?? crypto.randomUUID();
+ const toast = { ...opts, id };
+ toasts.value = [ ...toasts.value, toast ];
+ return id;
+}
+
+function updateToast(id: string, partial: Partial) {
+ toasts.value = toasts.value.map(toast => {
+ if (toast.id === id) {
+ return { ...toast, ...partial }
+ }
+ return toast;
+ })
+}
+
+export function removeToastFromStore(id: string) {
+ toasts.value = toasts.value.filter(toast => toast.id !== id);
+}
+//#endregion
+
export default {
showMessage,
showError,
diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css
index e395dbbb3..ade2687fb 100644
--- a/apps/client/src/stylesheets/style.css
+++ b/apps/client/src/stylesheets/style.css
@@ -1135,13 +1135,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
margin: 0 12px;
}
-#toast-container {
- position: absolute;
- width: 100%;
- top: 20px;
- pointer-events: none;
-}
-
.toast {
--bs-toast-bg: var(--accented-background-color);
--bs-toast-color: var(--main-text-color);
diff --git a/apps/client/src/widgets/Toast.css b/apps/client/src/widgets/Toast.css
new file mode 100644
index 000000000..720a5c0d4
--- /dev/null
+++ b/apps/client/src/widgets/Toast.css
@@ -0,0 +1,11 @@
+#toast-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ position: absolute;
+ width: 100%;
+ top: 20px;
+ pointer-events: none;
+ contain: none;
+}
diff --git a/apps/client/src/widgets/Toast.tsx b/apps/client/src/widgets/Toast.tsx
new file mode 100644
index 000000000..643cad551
--- /dev/null
+++ b/apps/client/src/widgets/Toast.tsx
@@ -0,0 +1,58 @@
+import "./Toast.css";
+
+import clsx from "clsx";
+import { useEffect } from "preact/hooks";
+
+import { removeToastFromStore, ToastOptionsWithRequiredId, toasts } from "../services/toast";
+import Icon from "./react/Icon";
+import { RawHtmlBlock } from "./react/RawHtml";
+
+const DEFAULT_DELAY = 3_000;
+
+export default function ToastContainer() {
+ return (
+
+ {toasts.value.map(toast => )}
+
+ )
+}
+
+function Toast({ id, title, autohide, delay, progress, message, icon }: ToastOptionsWithRequiredId) {
+ // Autohide.
+ useEffect(() => {
+ if (!autohide || !id) return;
+ const timeout = setTimeout(() => removeToastFromStore(id), delay || DEFAULT_DELAY);
+ return () => clearTimeout(timeout);
+ }, [ autohide, id, delay ]);
+
+ const closeButton = ;
+ const toastIcon = ;
+
+ return (
+
+ {title ? (
+
+ ) : (
+
{toastIcon}
+ )}
+
+
+
+ {!title && }
+
+
+ )
+}
diff --git a/apps/server/src/assets/views/desktop.ejs b/apps/server/src/assets/views/desktop.ejs
index 0a25c0625..8d53b914c 100644
--- a/apps/server/src/assets/views/desktop.ejs
+++ b/apps/server/src/assets/views/desktop.ejs
@@ -22,8 +22,6 @@
document.getElementsByTagName("body")[0].style.display = "none";
-
-
<%- include("./partials/windowGlobal.ejs", locals) %>
diff --git a/apps/server/src/assets/views/mobile.ejs b/apps/server/src/assets/views/mobile.ejs
index 0eaa11736..a182931e5 100644
--- a/apps/server/src/assets/views/mobile.ejs
+++ b/apps/server/src/assets/views/mobile.ejs
@@ -105,8 +105,6 @@
>
-
-
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 79a222b34..d8158dd7b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -190,6 +190,9 @@ importers:
'@popperjs/core':
specifier: 2.11.8
version: 2.11.8
+ '@preact/signals':
+ specifier: 2.5.1
+ version: 2.5.1(preact@10.28.0)
'@triliumnext/ckeditor5':
specifier: workspace:*
version: link:../../packages/ckeditor5
@@ -3897,6 +3900,14 @@ packages:
'@babel/core': 7.x
vite: 2.x || 3.x || 4.x || 5.x || 6.x || 7.x
+ '@preact/signals-core@1.12.1':
+ resolution: {integrity: sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==}
+
+ '@preact/signals@2.5.1':
+ resolution: {integrity: sha512-VPjk5YFt7i11Fi4UK0tzaEe5xLwfhUxXL3l89ocxQ5aPz7bRo8M5+N73LjBMPklyXKYKz6YsNo4Smp8n6nplng==}
+ peerDependencies:
+ preact: 10.28.0
+
'@prefresh/babel-plugin@0.5.2':
resolution: {integrity: sha512-AOl4HG6dAxWkJ5ndPHBgBa49oo/9bOiJuRDKHLSTyH+Fd9x00shTXpdiTj1W41l6oQIwUOAgJeHMn4QwIDpHkA==}
@@ -15268,8 +15279,6 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-code-block@47.3.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies:
@@ -15488,8 +15497,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-editor-multi-root@47.3.0':
dependencies:
@@ -15512,8 +15519,6 @@ snapshots:
'@ckeditor/ckeditor5-table': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-emoji@47.3.0':
dependencies:
@@ -18466,6 +18471,13 @@ snapshots:
- preact
- supports-color
+ '@preact/signals-core@1.12.1': {}
+
+ '@preact/signals@2.5.1(preact@10.28.0)':
+ dependencies:
+ '@preact/signals-core': 1.12.1
+ preact: 10.28.0
+
'@prefresh/babel-plugin@0.5.2': {}
'@prefresh/core@1.5.5(preact@10.28.0)':