# How Meetily Manages System Tray and Notifications in Its Tauri App

> Discover how Meetily uses Rust modules in its Tauri app to manage system tray icons and notifications. Learn about dynamic menus and user-friendly notification handling.

- Repository: [Zackriya Solutions/meetily](https://github.com/Zackriya-Solutions/meetily)
- Tags: how-to-guide
- Published: 2026-07-19

---

**Meetily handles system tray and notifications through dedicated Rust modules in its Tauri 2.x backend, using `TrayIconBuilder` for dynamic, state-driven tray menus and a layered notification manager that respects user consent and Do-Not-Disturb preferences.**

The Meetily desktop application implements its **system tray and notifications** through focused Rust modules that keep UI logic out of the frontend bundle. The tray gives users persistent access to recording controls, while the notification stack delivers native OS alerts for transcription completion, meeting reminders, and recording lifecycle events. All logic lives in focused modules under `frontend/src-tauri/src/`, making the implementation both maintainable and platform-agnostic.

## System Tray Implementation in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs)

The tray module owns every aspect of the persistent icon, from initial setup to dynamic menu rebuilds. It mirrors the recording lifecycle so users can start, pause, resume, or stop a session without bringing the main window to the foreground.

### Bootstrapping the Tray with `create_tray()`

When the Tauri app launches, `create_tray()` (lines 19–34) assembles the initial icon and menu. According to the Meetily source code, it starts from a `RecordingState::Stopped` baseline and assumes recording is permitted (`can_record = true`) unless onboarding says otherwise. The function chains `TrayIconBuilder` to bind the tooltip, menu, and a click handler in one builder call.

```rust
use tauri::tray::TrayIconBuilder;
use tauri::AppHandle;

pub fn create_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
    let menu = build_menu(app, RecordingState::Stopped, true)?;
    TrayIconBuilder::with_id("main-tray")
        .tooltip("Meetily")
        .menu(&menu)
        .on_menu_event(|app, event| handle_menu_event(app, event))
        .build(app)?;
    Ok(())
}

```

### Dynamic Menu Generation Using `build_menu()`

Menus are never static. The `build_menu()` helper (lines 16–92) constructs a fresh `MenuBuilder` on every state change, enabling or disabling items based on the current `RecordingState` and the `can_record` onboarding guard. Intermediate states such as *Starting* or *Pausing* are rendered as disabled items so users see immediate visual feedback while async operations run.

- **Stopped** — shows *Start Recording*, *Open Main Window*, *Settings*, *Check for Updates*, and *Quit*.
- **Recording** — swaps in *Pause Recording* and *Stop Recording*.
- **Paused** — surfaces *Resume Recording* and *Stop Recording*.

### Async State Updates with `update_tray_menu_async()`

Recording commands run asynchronously, so the tray must display an intermediate menu while a request is in flight. `set_tray_state()` (lines 22–34) injects this transitional menu. Once the operation completes, `update_tray_menu_async()` (lines 94–114) queries `get_current_recording_state()` and `check_can_record()`, then rebuilds the final menu through `build_menu()`. A thin synchronous wrapper, `update_tray_menu()`, exposes the same refresh logic to callers already on the main thread.

### Dispatching User Actions Through `handle_menu_event()`

All menu clicks flow through `handle_menu_event()` (lines 37–53), which pattern-matches the item ID and delegates to dedicated handlers:

- **`toggle_recording_handler()`** — starts or stops the audio pipeline via `crate::audio::recording_commands`.
- **`pause_recording_handler()`** and **`resume_recording_handler()`** — transition the recorder between active and paused.
- **`stop_recording_handler()`** — finalizes the session and emits a `"recording-stop-complete"` event so the frontend can react.
- **`focus_main_window()`** (lines 94–104) — raises the main Tauri window when the user selects *Open Main Window*.

## Desktop Notification Architecture in Meetily

Notifications are handled by a two-tier stack: a high-level manager enforces user preferences, and a low-level handler talks directly to the OS through Tauri’s notification plugin.

### Coordinating Alerts with `NotificationManager`

`NotificationManager<R>` (lines 12–331 in [`frontend/src-tauri/src/notifications/manager.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/notifications/manager.rs)) is the single entry point for every alert. Its constructor (`new()`) loads persisted permissions through `ConsentManager`, initializes a `SystemNotificationHandler`, and stores both behind `Arc` wrappers for thread-safe sharing. The `initialize()` method requests system permission on first launch by calling `system_handler.request_permission()`.

The core method `show_notification()` (lines 80–98) runs a three-step guard:

1. Ensure the manager is initialized.
2. Evaluate `should_show_notification()` against DND mode, user preferences, and priority.
3. Forward the `Notification` object to the system handler if all checks pass.

Typed helpers such as `show_recording_started()`, `show_transcription_complete()`, and `show_meeting_reminder()` (lines 100–144) each inspect a specific preference flag inside `settings.notification_preferences` before delegating to `show_notification()`. This design keeps policy out of the UI and guarantees a single place to enforce rules.

### Displaying OS Notifications via `SystemNotificationHandler`

`SystemNotificationHandler<R>` (lines 8–98 in [`frontend/src-tauri/src/notifications/system.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/notifications/system.rs)) is a thin, platform-agnostic wrapper around Tauri’s notification API. Its `show_notification()` method (lines 20–47) builds a native alert through `app_handle.notification().builder()` and displays it immediately.

```rust
use tauri::Manager;

impl<R: tauri::Runtime> SystemNotificationHandler<R> {
    pub fn show_notification(&self, notification: Notification) -> Result<(), String> {
        if self.is_dnd_active() && self.should_respect_dnd(&notification) {
            return Ok(()); // silently drop during DND
        }

        self.app_handle
            .notification()
            .builder()
            .title(&notification.title)
            .body(&notification.body)
            .show()
            .map_err(|e| e.to_string())?;

        Ok(())
    }
}

```

On most platforms, `request_permission()` (lines 65–73) is effectively a no-op because Tauri automatically grants notification rights, so `initialize()` completes without friction.

### Respecting User Consent and Do-Not-Disturb Mode

Consent and preference state are persisted to a local JSON file ([`notifications.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/notifications.json)) managed by `ConsentManager`. The file tracks:

- `consent_given` — whether the user has opted into alerts.
- `system_permission_granted` — cached platform permission state.
- `manual_dnd_mode` — app-level Do-Not-Disturb toggle.
- `respect_do_not_disturb` — whether non-critical alerts should be suppressed.

The manager keeps an in-memory `Arc<RwLock<NotificationSettings>>` that is read by `should_show_notification()` and updated whenever the user changes settings in the preferences panel.

## How Meetily Coordinates System Tray and Notifications

These two subsystems are decoupled but synchronize through Tauri events and shared Rust commands. When a user clicks *Stop Recording* in the tray, `stop_recording_handler()` executes the same command used by the frontend UI and then emits `"recording-stop-complete"` (lines 94–96 in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs)). The frontend listens for this event to refresh the recordings list, and it may also call the notification manager to surface a native *Recording Stopped* alert.

Conversely, the notification manager can be invoked from any backend thread—such as when a long-running transcription job finishes—without ever touching the tray. This separation keeps the tray focused on quick user actions while the notification stack handles asynchronous, time-sensitive updates.

Front-end code can observe tray-initiated state changes by listening to the standard event bus:

```typescript
import { listen } from '@tauri-apps/api/event';

listen('recording-stop-complete', (event) => {
  console.log('Tray-triggered stop finished', event.payload);
  // Refresh UI or request a confirmation notification
});

```

## Summary

- The Meetily **system tray and notifications** stack lives entirely in the Tauri Rust backend under `frontend/src-tauri/src/`.
- **[`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs)** creates the tray icon with `TrayIconBuilder`, rebuilds context-aware menus via `build_menu()`, and routes clicks through `handle_menu_event()`.
- **[`frontend/src-tauri/src/notifications/manager.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/notifications/manager.rs)** centralizes policy: consent, DND, and typed helpers like `show_recording_started()`.
- **[`frontend/src-tauri/src/notifications/system.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/notifications/system.rs)** is the low-level Tauri notification plugin wrapper that actually renders OS alerts.
- Tray handlers emit Tauri events (e.g., `"recording-stop-complete"`) so the frontend stays synchronized without direct coupling to the tray logic.

## Frequently Asked Questions

### How does Meetily update the tray menu when recording starts or stops?

The tray calls `update_tray_menu_async()` (lines 94–114), which fetches the current `RecordingState` via `get_current_recording_state()` and checks `check_can_record()`. It then rebuilds the menu through `build_menu()` so items like *Pause Recording* or *Stop Recording* appear or disappear instantly.

### Can Meetily show notifications if the user enables Do-Not-Disturb?

It depends on priority. `SystemNotificationHandler` checks `is_dnd_active()` and `should_respect_dnd()` before displaying any alert. Critical notifications can bypass DND, while standard recording or transcription alerts are suppressed until DND is turned off.

### Where does Meetily store notification preferences?

Preferences and consent are written to a local [`notifications.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/notifications.json) file by `ConsentManager`. The `NotificationManager` mirrors this data in an `Arc<RwLock<NotificationSettings>>` so decisions happen in memory without blocking on disk I/O.

### Does the tray menu work during the onboarding flow?

Yes, but with restricted actions. `build_menu()` accepts a `can_record` flag that is set by `check_can_record()` during onboarding. If the user has not completed setup, recording items are disabled, preventing premature audio capture while still allowing access to *Settings* and *Quit*.