diff --git a/src/docs/src/UI.md b/src/docs/src/UI.md index 70405e8c1..7b6e4b104 100644 --- a/src/docs/src/UI.md +++ b/src/docs/src/UI.md @@ -12,6 +12,7 @@ The UI API provides a comprehensive set of tools for creating rich user interfac ### Dialogs and Alerts - **[`puter.ui.alert()`](/UI/alert/)** - Show alert dialogs +- **[`puter.ui.notify()`](/UI/notify/)** - Show desktop notifications - **[`puter.ui.prompt()`](/UI/prompt/)** - Show input prompts ### Window Management @@ -47,4 +48,4 @@ The UI API provides a comprehensive set of tools for creating rich user interfac - **[`puter.ui.showColorPicker()`](/UI/showColorPicker/)** - Show color picker - **[`puter.ui.showFontPicker()`](/UI/showFontPicker/)** - Show font picker - **[`puter.ui.showSpinner()`](/UI/showSpinner/)** - Show spinner -- **[`puter.ui.socialShare()`](/UI/socialShare/)** - Share content socially \ No newline at end of file +- **[`puter.ui.socialShare()`](/UI/socialShare/)** - Share content socially diff --git a/src/docs/src/UI/notify.md b/src/docs/src/UI/notify.md new file mode 100644 index 000000000..d5adef88c --- /dev/null +++ b/src/docs/src/UI/notify.md @@ -0,0 +1,41 @@ +--- +title: puter.ui.notify() +description: Displays a desktop notification in Puter. +platforms: [apps] +--- + +Displays a desktop notification in Puter. Use this to surface app events without interrupting the user. + +## Syntax +```js +puter.ui.notify(options) +``` + +## Parameters + +#### `options` (optional) +An object that configures the notification. + +- `title` (string): Title shown in the notification. +- `text` (string): Body text shown under the title. +- `icon` (string): Icon URL or Puter icon name (for example `bell.svg`). +- `round_icon` (boolean): If `true`, renders the icon as a circle. `roundIcon` is accepted as an alias. +- `uid` (string): Optional ID to associate with the notification. +- `value` (any): Optional value stored on the notification element. + +## Return value +A `Promise` that resolves to the notification UID. + +## Examples +```html + + +``` diff --git a/src/docs/src/sidebar.js b/src/docs/src/sidebar.js index 687758bdb..45b4ef3f8 100755 --- a/src/docs/src/sidebar.js +++ b/src/docs/src/sidebar.js @@ -678,6 +678,14 @@ let sidebar = [ source: '/UI/alert.md', path: '/UI/alert', }, + { + title: 'notify()', + page_title: 'puter.ui.notify()', + title_tag: 'puter.ui.notify()', + icon: '/assets/img/function.svg', + source: '/UI/notify.md', + path: '/UI/notify', + }, { title: 'contextMenu()', page_title: 'puter.ui.contextMenu()', diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js index 6e08d3b35..d7dcba448 100644 --- a/src/gui/src/IPC.js +++ b/src/gui/src/IPC.js @@ -34,6 +34,7 @@ import UIWindowFontPicker from './UI/UIWindowFontPicker.js'; import UIWindowRequestPermission from './UI/UIWindowRequestPermission.js'; import UIWindowSaveAccount from './UI/UIWindowSaveAccount.js'; import UIWindowSignup from './UI/UIWindowSignup.js'; +import UINotification from './UI/UINotification.js'; import { PROCESS_IPC_ATTACHED } from './definitions.js'; import TeePromise from './util/TeePromise.js'; @@ -253,6 +254,38 @@ const ipc_listener = async (event, handled) => { }, '*'); } //-------------------------------------------------------- + // showNotification + //-------------------------------------------------------- + else if ( event.data.msg === 'showNotification' ) { + const options = event.data.options ?? {}; + const notification_uid = options.uid ?? `app-${app_uuid}-${msg_id}`; + let icon = window.icons['bell.svg']; + let round_icon = false; + + if ( typeof options.icon === 'string' && options.icon.length > 0 ) { + icon = window.icons[options.icon] ?? options.icon; + } + + if ( options.round_icon ) { + round_icon = true; + } + + UINotification({ + title: options.title ?? app_name ?? 'Notification', + text: options.text ?? '', + icon, + round_icon, + uid: notification_uid, + value: options.value, + }); + + target_iframe.contentWindow.postMessage({ + original_msg_id: msg_id, + msg: 'notificationShown', + uid: notification_uid, + }, '*'); + } + //-------------------------------------------------------- // getLanguage //-------------------------------------------------------- else if ( event.data.msg === 'getLanguage' ) { @@ -1488,12 +1521,14 @@ const ipc_listener = async (event, handled) => { target_path, el_filedialog_window, file_to_upload, overwrite, }) => { - const res = await puter.fs.write(target_path, - file_to_upload, - { - dedupeName: false, - overwrite: overwrite, - }); + const res = await puter.fs.write( + target_path, + file_to_upload, + { + dedupeName: false, + overwrite: overwrite, + }, + ); await tell_caller_and_update_views({ res, el_filedialog_window, target_path }); }; diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js index f299ecc27..e0320618f 100644 --- a/src/puter-js/src/modules/UI.js +++ b/src/puter-js/src/modules/UI.js @@ -499,6 +499,9 @@ class UI extends EventListener { // execute callback this.#callbackFunctions[e.data.original_msg_id](e.data.response); } + else if ( e.data.msg === 'notificationShown' ) { + this.#callbackFunctions[e.data.original_msg_id](e.data.uid); + } else if ( e.data.msg === 'languageReceived' ) { // execute callback this.#callbackFunctions[e.data.original_msg_id](e.data.language); @@ -722,6 +725,16 @@ class UI extends EventListener { }); }; + notify (options) { + return new Promise((resolve) => { + const normalized = { ...(options ?? {}) }; + if ( normalized.roundIcon !== undefined && normalized.round_icon === undefined ) { + normalized.round_icon = normalized.roundIcon; + } + this.#postMessageWithCallback('showNotification', resolve, { options: normalized }); + }); + }; + showDirectoryPicker (options, callback) { return new Promise((resolve, reject) => { if ( ! globalThis.open ) { diff --git a/src/puter-js/types/modules/ui.d.ts b/src/puter-js/types/modules/ui.d.ts index 60530d685..511449b94 100644 --- a/src/puter-js/types/modules/ui.d.ts +++ b/src/puter-js/types/modules/ui.d.ts @@ -74,6 +74,16 @@ export interface DirectoryPickerOptions { multiple?: boolean; } +export interface NotificationOptions { + title?: string; + text?: string; + icon?: string; + round_icon?: boolean; + roundIcon?: boolean; + uid?: string; + value?: unknown; +} + export interface AppConnectionCloseEvent { appInstanceID: string; statusCode?: number; @@ -113,6 +123,7 @@ export class AppConnection { export class UI { alert (message?: string, buttons?: AlertButton[]): Promise; prompt (message?: string, placeholder?: string): Promise; + notify (options?: NotificationOptions): Promise; authenticateWithPuter (): Promise; contextMenu (options: ContextMenuOptions): void; createWindow (options?: WindowOptions): void;