Skip to content

feat: feature announcement toast#2642

Open
janburzinski wants to merge 21 commits into
mainfrom
emdash/changelog-announcement-s8i8c
Open

feat: feature announcement toast#2642
janburzinski wants to merge 21 commits into
mainfrom
emdash/changelog-announcement-s8i8c

Conversation

@janburzinski

Copy link
Copy Markdown
Collaborator

Description

  • adds remote-configured in-app announcement

  • add dev controls to preview/show

  • app fetches feature-announcement.toml from main on startup

  • if enabled=true, the app version matches minAppVersion, and the announcement id was not dismissed before, it shows a toast

  • dismissed announcement ids are stored

Screenshot/Recording (if applicable)

https://streamable.com/qlkocf

Checklist
  • I kept this PR small and focused
  • I ran a self-review before opening this PR
  • I ran the relevant local checks or explained why not
  • I updated docs when behavior or setup changed
  • I added or updated tests when behavior changed, or explained why not
  • I only added comments where the logic is not obvious
  • I used Conventional Commits for commit
    messages and, when possible, the PR title

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a remote-configured in-app feature announcement system that fetches a TOML manifest from the main GitHub branch at startup and shows a toast if the announcement is enabled, the app version meets the minimum, and the user hasn't dismissed it before.

  • Remote manifest fetching: FeatureAnnouncementsService fetches feature-announcements.toml from GitHub raw content with a 15-minute in-process cache, falling back gracefully on network or parse errors.
  • Dismissal persistence: Dismissed announcement IDs are stored in the app settings store (announcements.dismissedIds), and fresh installs automatically pre-dismiss the current announcement to avoid greeting new users with stale content.
  • Dev controls: A AnnouncementDevControls component in the Settings page (rendered only in DEV builds) allows previewing and resetting the announcement without affecting the persisted state.

Confidence Score: 5/5

Safe to merge; the announcement feature is well-isolated, schema-validated on both the Zod and JSON Schema sides, and has good test coverage across store, state, schema, and media layers.

The two findings are both style/hardening concerns: an observable mutation that should use runInAction for consistency, and an unguarded async call whose failure path is already silently swallowed. Neither causes incorrect behaviour in the current configuration and neither is on the happy-path for end users.

apps/emdash-desktop/src/renderer/features/feature-announcements/feature-announcement-store.ts — the two suggestions in clearDismissal and loadDismissalState are worth a quick look before merge.

Important Files Changed

Filename Overview
apps/emdash-desktop/src/renderer/features/feature-announcements/feature-announcement-store.ts Core MobX store orchestrating manifest fetch, dismissal state, and presentation logic; clearDismissal() mutates the dismissedIds observable directly without runInAction, inconsistent with every other async mutation in the file.
apps/emdash-desktop/src/main/core/feature-announcements/service.ts Fetches and parses the remote TOML manifest with a 15-minute cache; correctly handles network failures, parse errors, and semver version gating.
apps/emdash-desktop/src/renderer/features/feature-announcements/feature-announcement-state.ts Pure async helpers for reading/writing dismissal state; well-tested, injectable client pattern makes unit testing straightforward.
apps/emdash-desktop/src/renderer/features/feature-announcements/feature-announcement-toast.tsx Renders the custom Sonner toast card; routes CTA actions (external URL or in-app navigation) and exposes dismiss/learn-more handlers correctly.
apps/emdash-desktop/src/shared/feature-announcements/schema.ts Zod schema with strict mode and a refine that correctly rejects CTAs with both or neither of action/url; exports typed parse helpers for enabled-only vs. raw (preview) manifests.
apps/emdash-desktop/src/renderer/features/feature-announcements/feature-announcement-launcher.tsx Observer component that fires the toast after an 800 ms delay once the workspace is visible and all preconditions are met; correctly cleans up the timer on unmount or condition change.
apps/emdash-desktop/src/renderer/features/settings/components/AnnouncementDevControls.tsx DEV-only UI for previewing and resetting the announcement; gated by import.meta.env.DEV at render time so it never appears in production builds.
apps/emdash-desktop/src/shared/feature-announcements/schema.test.ts Covers enabled/disabled manifests, legacy field rejection, and dual-CTA rejection; good coverage of the refine edge cases.
apps/emdash-desktop/src/renderer/features/feature-announcements/feature-announcement-store.test.ts Tests dismissal logic, preview-mode persistence guard, and the fresh-install offline scenario; comprehensive coverage of the store's public surface.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant R as Renderer (main.tsx)
    participant S as FeatureAnnouncementStore
    participant M as Main Process (service.ts)
    participant GH as GitHub Raw Content
    participant AS as AppSettings Store

    R->>S: "start({ isFreshInstall })"
    par Parallel fetch
        S->>M: rpc.featureAnnouncements.getCurrent()
        M->>GH: fetch feature-announcements.toml
        GH-->>M: TOML content
        M-->>S: "manifest | null"
    and Load dismissal state
        S->>AS: rpc.appSettings.get('announcements')
        AS-->>S: "{ initialized, dismissedIds }"
    end

    alt "isFreshInstall && !initialized"
        S->>AS: "write { initialized: true, dismissedIds: [manifest?.id] }"
        S->>AS: reload dismissal state
    end

    Note over S: status = 'ready', manifest set

    R->>S: FeatureAnnouncementLauncher observes store
    alt "shouldPresent (manifest exists && id not dismissed)"
        Note over R: setTimeout 800ms
        R->>S: markPresented()
        S->>AS: markAnnouncementDismissed(id)
        R->>R: showFeatureAnnouncementToast(manifest)
    end

    alt User clicks CTA action
        R->>R: appState.navigation.navigate('automations')
    else User clicks Learn More
        R->>R: confirmOpenExternalLink(learnMoreUrl)
    else User dismisses
        R->>R: toast.dismiss()
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant R as Renderer (main.tsx)
    participant S as FeatureAnnouncementStore
    participant M as Main Process (service.ts)
    participant GH as GitHub Raw Content
    participant AS as AppSettings Store

    R->>S: "start({ isFreshInstall })"
    par Parallel fetch
        S->>M: rpc.featureAnnouncements.getCurrent()
        M->>GH: fetch feature-announcements.toml
        GH-->>M: TOML content
        M-->>S: "manifest | null"
    and Load dismissal state
        S->>AS: rpc.appSettings.get('announcements')
        AS-->>S: "{ initialized, dismissedIds }"
    end

    alt "isFreshInstall && !initialized"
        S->>AS: "write { initialized: true, dismissedIds: [manifest?.id] }"
        S->>AS: reload dismissal state
    end

    Note over S: status = 'ready', manifest set

    R->>S: FeatureAnnouncementLauncher observes store
    alt "shouldPresent (manifest exists && id not dismissed)"
        Note over R: setTimeout 800ms
        R->>S: markPresented()
        S->>AS: markAnnouncementDismissed(id)
        R->>R: showFeatureAnnouncementToast(manifest)
    end

    alt User clicks CTA action
        R->>R: appState.navigation.navigate('automations')
    else User clicks Learn More
        R->>R: confirmOpenExternalLink(learnMoreUrl)
    else User dismisses
        R->>R: toast.dismiss()
    end
Loading

Reviews (4): Last reviewed commit: "test(announcements): type schema require..." | Re-trigger Greptile

Comment thread apps/emdash-desktop/feature-announcements.schema.json
@janburzinski

Copy link
Copy Markdown
Collaborator Author

@greptileai

@janburzinski

Copy link
Copy Markdown
Collaborator Author

@greptileai

@janburzinski

Copy link
Copy Markdown
Collaborator Author

@greptileai

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant