>;
+}> = ({ href, label, icon: Icon }) => (
+ dispatchExternalLinkClick(e, href)}
+ onAuxClick={(e) => dispatchExternalLinkClick(e, href)}
+ aria-label={label}
+ className="rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
+ >
+
+
+);
+
+export function SubscribeCard({
+ version,
+ onOpenReleaseNotes,
+ onClose,
+ store = subscribeCardStore,
+ autoDismissMs = SUCCESS_AUTO_DISMISS_MS,
+}: {
+ version: string;
+ onOpenReleaseNotes: () => void;
+ onClose: () => void;
+ store?: SubscribeCardStore;
+ autoDismissMs?: number;
+}) {
+ const { t } = useLingui();
+ const [succeeded, setSucceeded] = useState(false);
+
+ useEffect(() => {
+ if (!succeeded) return;
+ const timer = setTimeout(onClose, autoDismissMs);
+ return () => clearTimeout(timer);
+ }, [succeeded, onClose, autoDismissMs]);
+
+ return (
+
+
+ Product updates in your inbox.}
+ onSuccess={() => {
+ store.markSubscribed();
+ setSucceeded(true);
+ }}
+ onDismiss={() => {
+ store.dismiss();
+ onClose();
+ }}
+ />
+ {succeeded ? null : (
+
+
+ Follow us on
+
+
+
+
+
+ )}
+
+
+
+ Updated to Version {version}
+
+
+ Release notes
+
+
+
+ );
+}
diff --git a/packages/app/src/components/SubscribeForm.tsx b/packages/app/src/components/SubscribeForm.tsx
index 0104d3b60..87db31a71 100644
--- a/packages/app/src/components/SubscribeForm.tsx
+++ b/packages/app/src/components/SubscribeForm.tsx
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
-import { Check, Loader2, Mail, X } from 'lucide-react';
+import { ArrowRight, Check, Loader2, Mail, X } from 'lucide-react';
import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
@@ -20,6 +20,7 @@ export interface SubscribeFormProps {
onSuccess?: () => void;
onDismiss?: () => void;
autoFocus?: boolean;
+ compactSubmit?: boolean;
className?: string;
}
@@ -29,6 +30,7 @@ export function SubscribeForm({
onSuccess,
onDismiss,
autoFocus,
+ compactSubmit,
className,
}: SubscribeFormProps) {
const { t } = useLingui();
@@ -79,7 +81,7 @@ export function SubscribeForm({
{subscribed ? (
@@ -94,7 +96,7 @@ export function SubscribeForm({
before the success text replaces the description — adding the role
and the new content in the same render makes screen readers miss
the announcement (WCAG 4.1.3). */}
-
+
{subscribed ? (
Thanks for subscribing. Watch your inbox for product updates.
) : (
@@ -143,7 +145,11 @@ export function SubscribeForm({
{isSubmitting ? (
@@ -153,6 +159,13 @@ export function SubscribeForm({
Subscribing...
>
+ ) : compactSubmit ? (
+ <>
+
+
+ Subscribe
+
+ >
) : (
Subscribe
)}
diff --git a/packages/app/src/components/UpdateNotices.shared.ts b/packages/app/src/components/UpdateNotices.shared.ts
index ae0a47673..854cc32a6 100644
--- a/packages/app/src/components/UpdateNotices.shared.ts
+++ b/packages/app/src/components/UpdateNotices.shared.ts
@@ -43,6 +43,8 @@ export interface UpdateNotice {
onDismiss?: () => void;
variant?: 'info' | 'error' | 'success';
priority: number;
+ whatsNew?: { version: string; releaseUrl: string };
+ combinedSubscribe?: boolean;
dismissible?: boolean;
}
@@ -63,6 +65,10 @@ export function attachUpdateSubscribers(
addNotice: AddNoticeFn,
dismissNotice: DismissNoticeFn = () => {},
autoDismissMs: number = WHATS_NEW_AUTO_DISMISS_MS,
+ subscribeCombined: {
+ isEligible: (version: string) => boolean;
+ onShown: (version: string) => void;
+ } = { isEligible: () => false, onShown: () => {} },
): () => void {
const unsubscribers: Array<() => void> = [];
const autoDismissTimers = new Set>();
@@ -167,6 +173,26 @@ export function attachUpdateSubscribers(
unsubscribers.push(
bridge.onWhatsNew(({ version, releaseUrl }) => {
+ if (subscribeCombined.isEligible(version)) {
+ subscribeCombined.onShown(version);
+ addNotice({
+ id: `whats-new-combined-${version}`,
+ body: toastBBody(version),
+ variant: 'success',
+ priority: PRIORITY_WHATS_NEW,
+ combinedSubscribe: true,
+ whatsNew: { version, releaseUrl },
+ action: {
+ label: TOAST_B_ACTION,
+ onClick: () => {
+ void bridge.shell.openExternal(releaseUrl);
+ },
+ },
+ });
+ void bridge.update.dismissWhatsNew(version);
+ return;
+ }
+
const noticeId = `whats-new-${version}`;
addNotice({
id: noticeId,
diff --git a/packages/app/src/components/UpdateNotices.test.ts b/packages/app/src/components/UpdateNotices.test.ts
index c2308372f..7fcec821d 100644
--- a/packages/app/src/components/UpdateNotices.test.ts
+++ b/packages/app/src/components/UpdateNotices.test.ts
@@ -552,6 +552,62 @@ describe('Notice B — ok:update:whats-new', () => {
});
});
+describe('Notice B — combined subscribe path', () => {
+ test('eligible → combined notice: distinct id, whatsNew data, onShown + dismissWhatsNew at creation, no auto-dismiss', async () => {
+ const bridge = makeFakeBridge();
+ const addNotice = mock<(notice: UpdateNotice) => void>(() => {});
+ const dismissNotice = mock<(id: string) => void>(() => {});
+ const onShown = mock<(version: string) => void>(() => {});
+ attachUpdateSubscribers(castBridge(bridge), addNotice, dismissNotice, 15, {
+ isEligible: () => true,
+ onShown,
+ });
+ bridge._whatsNew?.({ version: '1.4.0', releaseUrl: 'https://example.com/r' });
+
+ const notice = addNotice.mock.calls[0]?.[0] as UpdateNotice;
+ expect(notice.id).toBe('whats-new-combined-1.4.0');
+ expect(notice.combinedSubscribe).toBe(true);
+ expect(notice.whatsNew).toEqual({ version: '1.4.0', releaseUrl: 'https://example.com/r' });
+ expect(onShown).toHaveBeenCalledWith('1.4.0');
+ expect(bridge.update.dismissWhatsNew).toHaveBeenCalledWith('1.4.0');
+ await new Promise((resolve) => setTimeout(resolve, 45));
+ expect(dismissNotice).not.toHaveBeenCalled();
+ });
+
+ test('ineligible → plain notice with auto-dismiss (combined branch skipped, onShown not called)', async () => {
+ const bridge = makeFakeBridge();
+ const addNotice = mock<(notice: UpdateNotice) => void>(() => {});
+ const dismissNotice = mock<(id: string) => void>(() => {});
+ const onShown = mock<(version: string) => void>(() => {});
+ attachUpdateSubscribers(castBridge(bridge), addNotice, dismissNotice, 15, {
+ isEligible: () => false,
+ onShown,
+ });
+ bridge._whatsNew?.({ version: '1.4.0', releaseUrl: 'https://example.com/r' });
+
+ const notice = addNotice.mock.calls[0]?.[0] as UpdateNotice;
+ expect(notice.id).toBe('whats-new-1.4.0');
+ expect(notice.combinedSubscribe).toBeUndefined();
+ expect(onShown).not.toHaveBeenCalled();
+ await new Promise((resolve) => setTimeout(resolve, 45));
+ expect(dismissNotice).toHaveBeenCalledWith('whats-new-1.4.0');
+ });
+
+ test('onWhatsNewDismissed echo does not remove the combined card (distinct id is load-bearing)', () => {
+ const bridge = makeFakeBridge();
+ const addNotice = mock<(notice: UpdateNotice) => void>(() => {});
+ const dismissNotice = mock<(id: string) => void>(() => {});
+ attachUpdateSubscribers(castBridge(bridge), addNotice, dismissNotice, 60_000, {
+ isEligible: () => true,
+ onShown: () => {},
+ });
+ bridge._whatsNew?.({ version: '1.4.0', releaseUrl: 'https://example.com/r' });
+ bridge._whatsNewDismissed?.({ version: '1.4.0' });
+ expect(dismissNotice).toHaveBeenCalledWith('whats-new-1.4.0');
+ expect(dismissNotice).not.toHaveBeenCalledWith('whats-new-combined-1.4.0');
+ });
+});
+
describe('Notice C — ok:update:stuck-hint', () => {
test('emits notice with D12 copy + download URL action', () => {
const bridge = makeFakeBridge();
diff --git a/packages/app/src/components/UpdateNotices.tsx b/packages/app/src/components/UpdateNotices.tsx
index b6b94cbb5..ca6721492 100644
--- a/packages/app/src/components/UpdateNotices.tsx
+++ b/packages/app/src/components/UpdateNotices.tsx
@@ -6,6 +6,7 @@ import type { ReactNode } from 'react';
import { useSyncExternalStore } from 'react';
import { Button } from '@/components/ui/button';
import { dismissNotice, getNoticesSnapshot, subscribeToNotices } from '@/lib/update-notices-store';
+import { SubscribeCard } from './SubscribeCard';
import type { UpdateNotice } from './UpdateNotices.shared';
export {
@@ -131,6 +132,17 @@ export function UpdateNotices(): ReactNode {
const notices = useSyncExternalStore(subscribeToNotices, getNoticesSnapshot, getNoticesSnapshot);
const active = pickActiveNotice(notices);
if (!active) return null;
+ if (active.combinedSubscribe && active.whatsNew) {
+ return (
+
+ active.action?.onClick()}
+ onClose={() => dismissNotice(active.id)}
+ />
+
+ );
+ }
return (
= {}): SubscribeCardStorage & {
+ raw(): string | null;
+} {
+ const data = new Map(Object.entries(initial));
+ return {
+ getItem(key) {
+ return data.get(key) ?? null;
+ },
+ setItem(key, value) {
+ data.set(key, value);
+ },
+ raw() {
+ return data.get(SUBSCRIBE_CARD_STORAGE_KEY) ?? null;
+ },
+ };
+}
+
+function stateWith(overrides: Partial): SubscribeCardState {
+ return { ...DEFAULT_SUBSCRIBE_CARD_STATE, ...overrides };
+}
+
+describe('readPersistedState', () => {
+ test('absent key returns default', () => {
+ expect(readPersistedState(memoryStorage())).toEqual(DEFAULT_SUBSCRIBE_CARD_STATE);
+ });
+
+ test('round-trips the durable fields', () => {
+ const stored = { subscribed: true, dismissed: false, shownVersions: ['1.0.0', '1.1.0'] };
+ const s = memoryStorage({ [SUBSCRIBE_CARD_STORAGE_KEY]: JSON.stringify(stored) });
+ expect(readPersistedState(s)).toEqual(stored);
+ });
+
+ test('non-array shownVersions and non-string entries coerce safely', () => {
+ const s = memoryStorage({
+ [SUBSCRIBE_CARD_STORAGE_KEY]: JSON.stringify({
+ subscribed: 'yes',
+ dismissed: 1,
+ shownVersions: 'nope',
+ }),
+ });
+ expect(readPersistedState(s)).toEqual({
+ subscribed: false,
+ dismissed: false,
+ shownVersions: [],
+ });
+
+ const s2 = memoryStorage({
+ [SUBSCRIBE_CARD_STORAGE_KEY]: JSON.stringify({ shownVersions: ['1.0.0', 2, null, '1.1.0'] }),
+ });
+ expect(readPersistedState(s2).shownVersions).toEqual(['1.0.0', '1.1.0']);
+ });
+
+ test('corrupt JSON falls back to default', () => {
+ const s = memoryStorage({ [SUBSCRIBE_CARD_STORAGE_KEY]: '{not valid json' });
+ expect(readPersistedState(s)).toEqual(DEFAULT_SUBSCRIBE_CARD_STATE);
+ });
+});
+
+describe('writePersistedState', () => {
+ test('persists the state verbatim', () => {
+ const s = memoryStorage();
+ writePersistedState({ subscribed: false, dismissed: true, shownVersions: ['9.9.9'] }, s);
+ expect(JSON.parse(s.raw() as string)).toEqual({
+ subscribed: false,
+ dismissed: true,
+ shownVersions: ['9.9.9'],
+ });
+ });
+});
+
+describe('isSubscribeCombinedEligible', () => {
+ test('eligible for a fresh version when not subscribed/dismissed and under budget', () => {
+ expect(isSubscribeCombinedEligible(DEFAULT_SUBSCRIBE_CARD_STATE, '1.0.0')).toBe(true);
+ });
+
+ test('not eligible once subscribed or dismissed', () => {
+ expect(isSubscribeCombinedEligible(stateWith({ subscribed: true }), '1.0.0')).toBe(false);
+ expect(isSubscribeCombinedEligible(stateWith({ dismissed: true }), '1.0.0')).toBe(false);
+ });
+
+ test('not eligible for a version already shown (no re-nag on reopen)', () => {
+ expect(isSubscribeCombinedEligible(stateWith({ shownVersions: ['1.0.0'] }), '1.0.0')).toBe(
+ false,
+ );
+ expect(isSubscribeCombinedEligible(stateWith({ shownVersions: ['1.0.0'] }), '1.1.0')).toBe(
+ true,
+ );
+ });
+
+ test('not eligible once the version budget is spent', () => {
+ const spent = stateWith({ shownVersions: ['1.0.0', '1.1.0', '1.2.0'] });
+ expect(spent.shownVersions.length).toBe(MAX_SUBSCRIBE_CARD_SHOWS);
+ expect(isSubscribeCombinedEligible(spent, '1.3.0')).toBe(false);
+ });
+});
+
+describe('createSubscribeCardStore', () => {
+ test('markSubscribed latches and is idempotent', () => {
+ const s = memoryStorage();
+ const store = createSubscribeCardStore(s);
+ store.markSubscribed();
+ store.markSubscribed();
+ expect(store.getSnapshot().subscribed).toBe(true);
+ expect(JSON.parse(s.raw() as string).subscribed).toBe(true);
+ });
+
+ test('dismiss latches and is idempotent', () => {
+ const store = createSubscribeCardStore(memoryStorage());
+ store.dismiss();
+ store.dismiss();
+ expect(store.getSnapshot().dismissed).toBe(true);
+ });
+
+ test('recordShown appends a version once; distinct versions accumulate', () => {
+ const s = memoryStorage();
+ const store = createSubscribeCardStore(s);
+ store.recordShown('1.0.0');
+ store.recordShown('1.0.0'); // idempotent per version
+ store.recordShown('1.1.0');
+ expect(store.getSnapshot().shownVersions).toEqual(['1.0.0', '1.1.0']);
+ expect(JSON.parse(s.raw() as string).shownVersions).toEqual(['1.0.0', '1.1.0']);
+ });
+
+ test('persisted shownVersions survive a fresh store over the same storage', () => {
+ const s = memoryStorage();
+ createSubscribeCardStore(s).recordShown('1.0.0');
+ const store2 = createSubscribeCardStore(s);
+ expect(store2.getSnapshot().shownVersions).toEqual(['1.0.0']);
+ expect(isSubscribeCombinedEligible(store2.getSnapshot(), '1.0.0')).toBe(false);
+ expect(isSubscribeCombinedEligible(store2.getSnapshot(), '2.0.0')).toBe(true);
+ });
+
+ test('subscribe notifies listeners', () => {
+ const store = createSubscribeCardStore(memoryStorage());
+ let calls = 0;
+ const unsub = store.subscribe(() => {
+ calls++;
+ });
+ store.markSubscribed();
+ store.markSubscribed(); // idempotent — no second notify
+ unsub();
+ store.dismiss(); // after unsub — not counted
+ expect(calls).toBe(1);
+ });
+});
diff --git a/packages/app/src/lib/subscribe-card-store.ts b/packages/app/src/lib/subscribe-card-store.ts
new file mode 100644
index 000000000..0fc1d37eb
--- /dev/null
+++ b/packages/app/src/lib/subscribe-card-store.ts
@@ -0,0 +1,137 @@
+export const SUBSCRIBE_CARD_STORAGE_KEY = 'ok-subscribe-card-v1';
+
+export const MAX_SUBSCRIBE_CARD_SHOWS = 3;
+
+export interface SubscribeCardState {
+ readonly subscribed: boolean;
+ readonly dismissed: boolean;
+ readonly shownVersions: readonly string[];
+}
+
+export const DEFAULT_SUBSCRIBE_CARD_STATE: SubscribeCardState = {
+ subscribed: false,
+ dismissed: false,
+ shownVersions: [],
+};
+
+export interface SubscribeCardStorage {
+ getItem(key: string): string | null;
+ setItem(key: string, value: string): void;
+}
+
+export interface SubscribeCardStore {
+ getSnapshot(): SubscribeCardState;
+ subscribe(listener: () => void): () => void;
+ markSubscribed(): void;
+ dismiss(): void;
+ recordShown(version: string): void;
+ install(): void;
+}
+
+export function isSubscribeCombinedEligible(state: SubscribeCardState, version: string): boolean {
+ return (
+ !state.subscribed &&
+ !state.dismissed &&
+ state.shownVersions.length < MAX_SUBSCRIBE_CARD_SHOWS &&
+ !state.shownVersions.includes(version)
+ );
+}
+
+function asFlag(value: unknown): boolean {
+ return value === true;
+}
+
+function asVersionList(value: unknown): string[] {
+ return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : [];
+}
+
+function coerceState(parsed: unknown): SubscribeCardState {
+ if (typeof parsed !== 'object' || parsed === null) return DEFAULT_SUBSCRIBE_CARD_STATE;
+ const obj = parsed as Record;
+ return {
+ subscribed: asFlag(obj.subscribed),
+ dismissed: asFlag(obj.dismissed),
+ shownVersions: asVersionList(obj.shownVersions),
+ };
+}
+
+export function readPersistedState(storage?: SubscribeCardStorage): SubscribeCardState {
+ try {
+ const s = storage ?? localStorage;
+ const raw = s.getItem(SUBSCRIBE_CARD_STORAGE_KEY);
+ if (raw == null) return DEFAULT_SUBSCRIBE_CARD_STATE;
+ return coerceState(JSON.parse(raw));
+ } catch (err) {
+ console.warn('[subscribe-card-store] readPersistedState failed (corrupt/privacy/SSR)', err);
+ return DEFAULT_SUBSCRIBE_CARD_STATE;
+ }
+}
+
+export function writePersistedState(
+ state: SubscribeCardState,
+ storage?: SubscribeCardStorage,
+): void {
+ try {
+ const s = storage ?? localStorage;
+ s.setItem(SUBSCRIBE_CARD_STORAGE_KEY, JSON.stringify(state));
+ } catch (err) {
+ console.warn('[subscribe-card-store] writePersistedState failed (quota/privacy/SSR)', err);
+ }
+}
+
+export function createSubscribeCardStore(storage?: SubscribeCardStorage): SubscribeCardStore {
+ let state = readPersistedState(storage);
+ const listeners = new Set<() => void>();
+ let installed = false;
+
+ function notify(): void {
+ for (const listener of listeners) listener();
+ }
+
+ function commit(next: SubscribeCardState): void {
+ state = next;
+ writePersistedState(state, storage);
+ notify();
+ }
+
+ return {
+ getSnapshot(): SubscribeCardState {
+ return state;
+ },
+
+ subscribe(listener): () => void {
+ listeners.add(listener);
+ return () => {
+ listeners.delete(listener);
+ };
+ },
+
+ markSubscribed(): void {
+ if (state.subscribed) return;
+ commit({ ...state, subscribed: true });
+ },
+
+ dismiss(): void {
+ if (state.dismissed) return;
+ commit({ ...state, dismissed: true });
+ },
+
+ recordShown(version): void {
+ if (state.shownVersions.includes(version)) return;
+ commit({ ...state, shownVersions: [...state.shownVersions, version] });
+ },
+
+ install(): void {
+ if (installed) return;
+ installed = true;
+ state = readPersistedState(storage);
+ notify();
+ },
+ };
+}
+
+export const subscribeCardStore: SubscribeCardStore = createSubscribeCardStore();
+
+export function installSubscribeCardStore(): void {
+ subscribeCardStore.install();
+}
diff --git a/packages/app/src/lib/update-notices-store.ts b/packages/app/src/lib/update-notices-store.ts
index e73e84358..6cbd9e185 100644
--- a/packages/app/src/lib/update-notices-store.ts
+++ b/packages/app/src/lib/update-notices-store.ts
@@ -3,6 +3,7 @@ import {
attachUpdateSubscribers,
type UpdateNotice,
} from '@/components/UpdateNotices.shared';
+import { isSubscribeCombinedEligible, subscribeCardStore } from '@/lib/subscribe-card-store';
let notices: UpdateNotice[] = [];
const listeners = new Set<() => void>();
@@ -48,7 +49,10 @@ export function installUpdateNoticesBridge(): void {
const bridge = window.okDesktop;
if (!bridge) return;
attached = true;
- attachUpdateSubscribers(bridge, addNotice, dismissNotice);
+ attachUpdateSubscribers(bridge, addNotice, dismissNotice, undefined, {
+ isEligible: (version) => isSubscribeCombinedEligible(subscribeCardStore.getSnapshot(), version),
+ onShown: (version) => subscribeCardStore.recordShown(version),
+ });
bridge.state.query().then(
(snapshot) => {
if (snapshot.schemaIncompatibility) {
diff --git a/packages/app/src/locales/en/messages.json b/packages/app/src/locales/en/messages.json
index a74758785..5d71456d3 100644
--- a/packages/app/src/locales/en/messages.json
+++ b/packages/app/src/locales/en/messages.json
@@ -123,6 +123,7 @@
"322lyb": ["Highlight tips, warnings, and notes."],
"34qyH5": ["Could not reach server"],
"36WXSC": ["Couldn't delete template: ", ["error"]],
+ "36aWJD": ["Join us on Discord"],
"379t2h": ["Unified diff"],
"38foVZ": ["Loading diff"],
"3DTatP": ["Copy a prompt for your agent, start a blank file, or scaffold from a template."],
@@ -400,6 +401,7 @@
"9npOH9": ["Open file"],
"9o1bIV": [["behind"], " behind"],
"9oE4C3": ["Browse your repos:"],
+ "9sBe85": ["Follow us on"],
"9tJX8r": ["By meaning"],
"9zb2WA": ["Connecting"],
"A1taO8": ["Search"],
@@ -687,6 +689,7 @@
"HyvSqq": ["Project Skill"],
"I1gRhX": ["Failed to rename"],
"I5AfhE": ["Sync status unavailable — server unreachable."],
+ "I7Exsw": ["Follow us on X"],
"IA1wNb": ["The Claude Desktop App didn't open the file. Is it installed? (", ["msg"], ")"],
"IA6JW2": ["Detecting"],
"ICoNYz": ["You can also manage this in Settings."],
@@ -1419,6 +1422,7 @@
"Add meaning-based ranking to search so conceptually-related pages surface even when they share no keywords. This setting applies only to this computer."
],
"dURmlQ": ["\"frontmatter\" is a reserved property name"],
+ "dbapvC": ["Follow us on social media"],
"dbhUX1": ["Hide this file"],
"dbtjJh": ["(opens in new tab)"],
"djc1l9": ["More than ", ["0"], " requests were blocked by the preview's security policy"],
@@ -1545,6 +1549,7 @@
"hGmuTc": ["Docs tagged #", ["tagDocsName"]],
"hHMv6l": ["Delete block"],
"hIQkLb": ["New chat"],
+ "hLy8b1": ["Updated to Version ", ["version"]],
"hSBIG-": ["A title, description, tags — anything that describes this folder."],
"hSySVc": ["Install handoff failed: ", ["msg"]],
"hXUWyd": ["Value is required"],
@@ -2144,6 +2149,7 @@
"yHikCO": ["Open CodeMirror's go-to-line prompt."],
"yIPXut": ["Preparing search"],
"yKu_3Y": ["Restore"],
+ "yLEyuq": ["Product updates in your inbox."],
"yLq4w4": ["Separator"],
"yMgL_E": ["Find field navigation"],
"yQE2r9": ["Loading"],
@@ -2205,6 +2211,7 @@
"}1> fills in on create."
],
"ztKII6": ["Failed to add item"],
- "zwBp5t": ["Private"]
+ "zwBp5t": ["Private"],
+ "zyKa-I": ["Star us on GitHub"]
}
}
diff --git a/packages/app/src/locales/en/messages.po b/packages/app/src/locales/en/messages.po
index 56931e48c..b57a53bf3 100644
--- a/packages/app/src/locales/en/messages.po
+++ b/packages/app/src/locales/en/messages.po
@@ -2945,6 +2945,18 @@ msgstr "Folder properties"
msgid "Folder target: {linkTitle}. Click to open the overview."
msgstr "Folder target: {linkTitle}. Click to open the overview."
+#: src/components/SubscribeCard.tsx
+msgid "Follow us on"
+msgstr "Follow us on"
+
+#: src/components/SubscribeCard.tsx
+msgid "Follow us on social media"
+msgstr "Follow us on social media"
+
+#: src/components/SubscribeCard.tsx
+msgid "Follow us on X"
+msgstr "Follow us on X"
+
#: src/editor/slash-command/items.tsx
msgid "Footnote"
msgstr "Footnote"
@@ -3448,6 +3460,10 @@ msgstr "Item {0}"
msgid "Items to be created"
msgstr "Items to be created"
+#: src/components/SubscribeCard.tsx
+msgid "Join us on Discord"
+msgstr "Join us on Discord"
+
#: src/editor/components/Pdf.tsx
msgid "Jump to page {pageNumber}"
msgstr "Jump to page {pageNumber}"
@@ -4869,6 +4885,10 @@ msgstr "probe failed"
msgid "Product updates"
msgstr "Product updates"
+#: src/components/SubscribeCard.tsx
+msgid "Product updates in your inbox."
+msgstr "Product updates in your inbox."
+
#: src/components/settings/ProjectTemplatesSection.tsx
msgid "project"
msgstr "project"
@@ -5077,6 +5097,7 @@ msgid "Relaunching to install the update…"
msgstr "Relaunching to install the update…"
#: src/components/settings/SettingsDialogShell.tsx
+#: src/components/SubscribeCard.tsx
msgid "Release notes"
msgstr "Release notes"
@@ -5965,6 +5986,10 @@ msgstr "Split diff"
msgid "Split list item"
msgstr "Split list item"
+#: src/components/SubscribeCard.tsx
+msgid "Star us on GitHub"
+msgstr "Star us on GitHub"
+
#: src/components/NavigatorApp.tsx
msgid "Start a new OpenKnowledge project."
msgstr "Start a new OpenKnowledge project."
@@ -6005,6 +6030,7 @@ msgstr "Starting sign-in flow"
msgid "status"
msgstr "status"
+#: src/components/SubscribeCard.tsx
#: src/components/SubscribeForm.tsx
msgid "Stay in the loop"
msgstr "Stay in the loop"
@@ -6826,6 +6852,10 @@ msgstr "Updated \"{display}\" to {0}."
msgid "updated folder properties"
msgstr "updated folder properties"
+#: src/components/SubscribeCard.tsx
+msgid "Updated to Version {version}"
+msgstr "Updated to Version {version}"
+
#: src/components/SkillUpdateDialog.tsx
msgid "Updating"
msgstr "Updating"
diff --git a/packages/app/src/locales/pseudo/messages.json b/packages/app/src/locales/pseudo/messages.json
index f39ed2a70..b39f87b95 100644
--- a/packages/app/src/locales/pseudo/messages.json
+++ b/packages/app/src/locales/pseudo/messages.json
@@ -123,6 +123,7 @@
"322lyb": ["Ĥĩĝĥĺĩĝĥţ ţĩƥś, ŵàŕńĩńĝś, àńď ńōţēś."],
"34qyH5": ["Ćōũĺď ńōţ ŕēàćĥ śēŕvēŕ"],
"36WXSC": ["Ćōũĺďń'ţ ďēĺēţē ţēḿƥĺàţē: ", ["error"]],
+ "36aWJD": ["ĵōĩń ũś ōń Ďĩśćōŕď"],
"379t2h": ["Ũńĩƒĩēď ďĩƒƒ"],
"38foVZ": ["Ĺōàďĩńĝ ďĩƒƒ"],
"3DTatP": ["Ćōƥŷ à ƥŕōḿƥţ ƒōŕ ŷōũŕ àĝēńţ, śţàŕţ à ƀĺàńķ ƒĩĺē, ōŕ śćàƒƒōĺď ƒŕōḿ à ţēḿƥĺàţē."],
@@ -400,6 +401,7 @@
"9npOH9": ["Ōƥēń ƒĩĺē"],
"9o1bIV": [["behind"], " ƀēĥĩńď"],
"9oE4C3": ["ßŕōŵśē ŷōũŕ ŕēƥōś:"],
+ "9sBe85": ["Ƒōĺĺōŵ ũś ōń"],
"9tJX8r": ["ßŷ ḿēàńĩńĝ"],
"9zb2WA": ["Ćōńńēćţĩńĝ"],
"A1taO8": ["Śēàŕćĥ"],
@@ -687,6 +689,7 @@
"HyvSqq": ["ƤŕōĴēćţ Śķĩĺĺ"],
"I1gRhX": ["Ƒàĩĺēď ţō ŕēńàḿē"],
"I5AfhE": ["Śŷńć śţàţũś ũńàvàĩĺàƀĺē — śēŕvēŕ ũńŕēàćĥàƀĺē."],
+ "I7Exsw": ["Ƒōĺĺōŵ ũś ōń X"],
"IA1wNb": ["Ţĥē Ćĺàũďē Ďēśķţōƥ Àƥƥ ďĩďń'ţ ōƥēń ţĥē ƒĩĺē. Ĩś ĩţ ĩńśţàĺĺēď? (", ["msg"], ")"],
"IA6JW2": ["Ďēţēćţĩńĝ"],
"ICoNYz": ["Ŷōũ ćàń àĺśō ḿàńàĝē ţĥĩś ĩń Śēţţĩńĝś."],
@@ -1419,6 +1422,7 @@
"Àďď ḿēàńĩńĝ-ƀàśēď ŕàńķĩńĝ ţō śēàŕćĥ śō ćōńćēƥţũàĺĺŷ-ŕēĺàţēď ƥàĝēś śũŕƒàćē ēvēń ŵĥēń ţĥēŷ śĥàŕē ńō ķēŷŵōŕďś. Ţĥĩś śēţţĩńĝ àƥƥĺĩēś ōńĺŷ ţō ţĥĩś ćōḿƥũţēŕ."
],
"dURmlQ": ["\"ƒŕōńţḿàţţēŕ\" ĩś à ŕēśēŕvēď ƥŕōƥēŕţŷ ńàḿē"],
+ "dbapvC": ["Ƒōĺĺōŵ ũś ōń śōćĩàĺ ḿēďĩà"],
"dbhUX1": ["Ĥĩďē ţĥĩś ƒĩĺē"],
"dbtjJh": ["(ōƥēńś ĩń ńēŵ ţàƀ)"],
"djc1l9": ["Ḿōŕē ţĥàń ", ["0"], " ŕēǫũēśţś ŵēŕē ƀĺōćķēď ƀŷ ţĥē ƥŕēvĩēŵ'ś śēćũŕĩţŷ ƥōĺĩćŷ"],
@@ -1545,6 +1549,7 @@
"hGmuTc": ["Ďōćś ţàĝĝēď #", ["tagDocsName"]],
"hHMv6l": ["Ďēĺēţē ƀĺōćķ"],
"hIQkLb": ["Ńēŵ ćĥàţ"],
+ "hLy8b1": ["Ũƥďàţēď ţō Vēŕśĩōń ", ["version"]],
"hSBIG-": ["À ţĩţĺē, ďēśćŕĩƥţĩōń, ţàĝś — àńŷţĥĩńĝ ţĥàţ ďēśćŕĩƀēś ţĥĩś ƒōĺďēŕ."],
"hSySVc": ["Ĩńśţàĺĺ ĥàńďōƒƒ ƒàĩĺēď: ", ["msg"]],
"hXUWyd": ["Vàĺũē ĩś ŕēǫũĩŕēď"],
@@ -2144,6 +2149,7 @@
"yHikCO": ["Ōƥēń ĆōďēḾĩŕŕōŕ'ś ĝō-ţō-ĺĩńē ƥŕōḿƥţ."],
"yIPXut": ["Ƥŕēƥàŕĩńĝ śēàŕćĥ"],
"yKu_3Y": ["Ŕēśţōŕē"],
+ "yLEyuq": ["Ƥŕōďũćţ ũƥďàţēś ĩń ŷōũŕ ĩńƀōx."],
"yLq4w4": ["Śēƥàŕàţōŕ"],
"yMgL_E": ["Ƒĩńď ƒĩēĺď ńàvĩĝàţĩōń"],
"yQE2r9": ["Ĺōàďĩńĝ"],
@@ -2205,6 +2211,7 @@
"}1> ƒĩĺĺś ĩń ōń ćŕēàţē."
],
"ztKII6": ["Ƒàĩĺēď ţō àďď ĩţēḿ"],
- "zwBp5t": ["Ƥŕĩvàţē"]
+ "zwBp5t": ["Ƥŕĩvàţē"],
+ "zyKa-I": ["Śţàŕ ũś ōń ĜĩţĤũƀ"]
}
}
diff --git a/packages/app/src/locales/pseudo/messages.po b/packages/app/src/locales/pseudo/messages.po
index bb9a82c5f..9b41e1e0a 100644
--- a/packages/app/src/locales/pseudo/messages.po
+++ b/packages/app/src/locales/pseudo/messages.po
@@ -2940,6 +2940,18 @@ msgstr ""
msgid "Folder target: {linkTitle}. Click to open the overview."
msgstr ""
+#: src/components/SubscribeCard.tsx
+msgid "Follow us on"
+msgstr ""
+
+#: src/components/SubscribeCard.tsx
+msgid "Follow us on social media"
+msgstr ""
+
+#: src/components/SubscribeCard.tsx
+msgid "Follow us on X"
+msgstr ""
+
#: src/editor/slash-command/items.tsx
msgid "Footnote"
msgstr ""
@@ -3443,6 +3455,10 @@ msgstr ""
msgid "Items to be created"
msgstr ""
+#: src/components/SubscribeCard.tsx
+msgid "Join us on Discord"
+msgstr ""
+
#: src/editor/components/Pdf.tsx
msgid "Jump to page {pageNumber}"
msgstr ""
@@ -4864,6 +4880,10 @@ msgstr ""
msgid "Product updates"
msgstr ""
+#: src/components/SubscribeCard.tsx
+msgid "Product updates in your inbox."
+msgstr ""
+
#: src/components/settings/ProjectTemplatesSection.tsx
msgid "project"
msgstr ""
@@ -5072,6 +5092,7 @@ msgid "Relaunching to install the update…"
msgstr ""
#: src/components/settings/SettingsDialogShell.tsx
+#: src/components/SubscribeCard.tsx
msgid "Release notes"
msgstr ""
@@ -5960,6 +5981,10 @@ msgstr ""
msgid "Split list item"
msgstr ""
+#: src/components/SubscribeCard.tsx
+msgid "Star us on GitHub"
+msgstr ""
+
#: src/components/NavigatorApp.tsx
msgid "Start a new OpenKnowledge project."
msgstr ""
@@ -6000,6 +6025,7 @@ msgstr ""
msgid "status"
msgstr ""
+#: src/components/SubscribeCard.tsx
#: src/components/SubscribeForm.tsx
msgid "Stay in the loop"
msgstr ""
@@ -6821,6 +6847,10 @@ msgstr ""
msgid "updated folder properties"
msgstr ""
+#: src/components/SubscribeCard.tsx
+msgid "Updated to Version {version}"
+msgstr ""
+
#: src/components/SkillUpdateDialog.tsx
msgid "Updating"
msgstr ""
diff --git a/packages/app/src/main.tsx b/packages/app/src/main.tsx
index ac459d6bb..b776e8cb0 100644
--- a/packages/app/src/main.tsx
+++ b/packages/app/src/main.tsx
@@ -30,6 +30,7 @@ import {
import { installRelaunchStateBridge } from '@/lib/relaunch-store';
import { installShareReceivedListener } from '@/lib/share/receive-store';
import { seedInitialDocHashFromWindow } from '@/lib/single-file-initial-doc';
+import { installSubscribeCardStore } from '@/lib/subscribe-card-store';
import { installUpdateNoticesBridge } from '@/lib/update-notices-store';
import { App } from './App';
import '@fontsource-variable/inter';
@@ -55,6 +56,8 @@ installUpdateNoticesBridge();
installOnboardingCardStore();
+installSubscribeCardStore();
+
installRelaunchStateBridge();
if (typeof window !== 'undefined') {