From ec40aa0838c63aba3326e86bdb92fa065bd0c848 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Tue, 10 Mar 2026 16:07:25 +0100 Subject: [PATCH] Add action buttons to flashNotification Used by upgrade notification --- client/client.ts | 6 +++++- client/components/top_bar.tsx | 33 ++++++++++++++++++++++++++++- client/editor_ui.tsx | 36 +++++++++++++++++++++++++------- client/plugos/syscalls/editor.ts | 4 +++- client/styles/top.scss | 30 +++++++++++++++++++++++++- plug-api/types/client.ts | 7 +++++++ website/API/editor.md | 25 +++++++++++++++++++++- website/CHANGELOG.md | 1 + 8 files changed, 130 insertions(+), 12 deletions(-) diff --git a/client/client.ts b/client/client.ts index 8d2e39df..08a8db90 100644 --- a/client/client.ts +++ b/client/client.ts @@ -822,8 +822,12 @@ export class Client { ) { this.versionMismatchNotified = true; this.ui.flashNotification( - "A new version of SilverBullet is available. Reload to update.", + "A new version of SilverBullet client is available. A reload or two is required to update.", "warning", + { + timeout: 0, + actions: [{ name: "Reload", run: () => location.reload() }], + }, ); } break; diff --git a/client/components/top_bar.tsx b/client/components/top_bar.tsx index 64f138ef..e5995346 100644 --- a/client/components/top_bar.tsx +++ b/client/components/top_bar.tsx @@ -18,6 +18,7 @@ export function TopBar({ isLoading, notifications, onRename, + onDismissNotification, actionButtons, darkMode, vimMode, @@ -40,6 +41,7 @@ export function TopBar({ progressPercentage?: number; progressType?: string; onRename: (newName?: string) => Promise; + onDismissNotification: (id: number) => void; onClick: () => void; actionButtons: ActionButton[]; lhs?: ComponentChildren; @@ -97,7 +99,36 @@ export function TopBar({ key={notification.id} className={`sb-notification-${notification.type}`} > - {notification.message} + + {notification.message} + + {notification.actions && notification.actions.length > 0 && ( + + {notification.actions.map((action, i) => ( + + ))} + + )} + {notification.persistent && ( + + )} ))} diff --git a/client/editor_ui.tsx b/client/editor_ui.tsx index 568625c8..90adf8d5 100644 --- a/client/editor_ui.tsx +++ b/client/editor_ui.tsx @@ -20,6 +20,7 @@ import { Panel } from "./components/panel.tsx"; import { safeRun } from "@silverbulletmd/silverbullet/lib/async"; import type { FilterOption, + NotificationAction, NotificationType, } from "@silverbulletmd/silverbullet/type/client"; import { notificationDismissTimeouts } from "@silverbulletmd/silverbullet/type/client"; @@ -86,8 +87,26 @@ export class MainUI { viewDispatch: (action: Action) => void = () => {}; - flashNotification(message: string, type: NotificationType = "info") { + flashNotification( + message: string, + type: NotificationType = "info", + options?: { + timeout?: number; + actions?: NotificationAction[]; + }, + ) { const id = Math.floor(Math.random() * 1000000); + const dismiss = () => { + this.viewDispatch({ type: "dismiss-notification", id }); + }; + const persistent = options?.timeout === 0; + const actions = options?.actions?.map((action) => ({ + name: action.name, + run: () => { + action.run(); + dismiss(); + }, + })); this.viewDispatch({ type: "show-notification", notification: { @@ -95,14 +114,14 @@ export class MainUI { type, message, date: new Date(), + actions, + persistent, }, }); - setTimeout(() => { - this.viewDispatch({ - type: "dismiss-notification", - id: id, - }); - }, notificationDismissTimeouts[type]); + if (!persistent) { + const timeout = options?.timeout ?? notificationDismissTimeouts[type]; + setTimeout(dismiss, timeout); + } } showProgress(progressPercentage?: number, progressType?: "sync" | "index") { @@ -386,6 +405,9 @@ export class MainUI { !viewState.current ? "" : getNameFromPath(viewState.current.path) } notifications={viewState.notifications} + onDismissNotification={(id) => { + dispatch({ type: "dismiss-notification", id }); + }} isOnline={viewState.isOnline} unsavedChanges={viewState.unsavedChanges} isLoading={viewState.isLoading} diff --git a/client/plugos/syscalls/editor.ts b/client/plugos/syscalls/editor.ts index 111e99dc..f0274952 100644 --- a/client/plugos/syscalls/editor.ts +++ b/client/plugos/syscalls/editor.ts @@ -22,6 +22,7 @@ import { getCM as vimGetCm, Vim } from "@replit/codemirror-vim"; import type { SysCallMapping } from "../system.ts"; import type { FilterOption, + NotificationAction, NotificationType, UploadFile, } from "@silverbulletmd/silverbullet/type/client"; @@ -289,8 +290,9 @@ export function editorSyscalls(client: Client): SysCallMapping { _ctx, message: string, type: NotificationType = "info", + options?: { timeout?: number; actions?: NotificationAction[] }, ) => { - client.ui.flashNotification(message, type); + client.ui.flashNotification(message, type, options); }, "editor.filterBox": ( _ctx, diff --git a/client/styles/top.scss b/client/styles/top.scss index 4042a6e1..db706e3c 100644 --- a/client/styles/top.scss +++ b/client/styles/top.scss @@ -34,9 +34,37 @@ z-index: 100; > div { - padding: 3px; + padding: 3px 8px; margin-bottom: 3px; border-radius: 5px; + display: flex; + align-items: center; + gap: 8px; + } + + .sb-notification-actions { + display: flex; + gap: 4px; + flex-shrink: 0; + + .sb-button { + font-size: 0.85em; + padding: 1px 6px; + white-space: nowrap; + } + } + + .sb-notification-dismiss { + background: none; + border: none; + color: inherit; + cursor: pointer; + font-size: 1.1em; + padding: 0 2px; + opacity: 0.7; + &:hover { + opacity: 1; + } } } } diff --git a/plug-api/types/client.ts b/plug-api/types/client.ts index 0d59d5cf..d15bd3a9 100644 --- a/plug-api/types/client.ts +++ b/plug-api/types/client.ts @@ -19,11 +19,18 @@ export const notificationDismissTimeouts: Record = { warning: 8000, }; +export type NotificationAction = { + name: string; + run: () => void; +}; + export type Notification = { id: number; message: string; type: NotificationType; date: Date; + actions?: NotificationAction[]; + persistent?: boolean; }; export type PanelMode = number; diff --git a/website/API/editor.md b/website/API/editor.md index 01fed0bb..2650d7f8 100644 --- a/website/API/editor.md +++ b/website/API/editor.md @@ -238,12 +238,35 @@ Example: editor.hidePanel("rhs") ``` -### editor.flashNotification(message, type) +### editor.flashNotification(message, type, options?) Shows a flash notification. +Parameters: +- `message`: The message to display +- `type`: Notification type — `"info"` (default), `"error"`, or `"warning"` +- `options` (optional): Table with additional options: + - `timeout`: Dismiss timeout in milliseconds. Use `0` for persistent notifications (default: 4000/5000/8000 depending on type) + - `actions`: List of action buttons, each with: + - `name`: Button label + - `run`: Function to call when clicked (auto-dismisses the notification) + Example: ```lua +-- Simple notification editor.flashNotification("Operation completed", "info") + +-- Persistent notification with action button +editor.flashNotification("Update available", "warning", { + timeout = 0, + actions = { + { + name = "Reload", + run = function() + editor.reloadUI() + end + } + } +}) ``` ### editor.downloadFile(filename, dataUrl) diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index acfd7a36..effd4b26 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -4,6 +4,7 @@ An attempt at documenting the changes/new features introduced in each release. Whenever a commit is pushed to the `main` branch, within ~5 minutes, it will be released as a docker image with the `:v2` tag, and a binary in the [edge release](https://github.com/silverbulletmd/silverbullet/releases/tag/edge). If you want to live on the bleeding edge of SilverBullet goodness (or regression) this is where to do it. * **Technical: Deno → Node.js migration**: The TypeScript/client codebase has been migrated from Deno to Node.js, now using vitest for tests. This _should_ purely be a tooling change, but may inadvertently have lead to regressions. Please report if you find anything. +* `editor.flashNotification` now supports an optional third argument with `timeout` (use `0` for persistent notifications) and `actions` (buttons with callbacks). * [[Outlines]] commands have been thoroughly reworked. Should now be more robust and better tested outline move/indent operations. New features: * Now also works with numbered items (and renumbers them) * Now works with headers (moves around entire sections)