From a6541b9731f84f91b8453e1a82ef7fb5dd541045 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:13:05 +0500 Subject: [PATCH 1/8] web: show monograph's view count Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- .../web/src/components/publish-view/index.tsx | 38 ++++++++++++++++--- packages/core/src/collections/monographs.ts | 1 + packages/core/src/database/migrations.ts | 8 ++++ packages/core/src/types.ts | 1 + packages/intl/locale/en.po | 4 ++ packages/intl/locale/pseudo-LOCALE.po | 4 ++ packages/intl/src/strings.ts | 7 +++- 7 files changed, 57 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/publish-view/index.tsx b/apps/web/src/components/publish-view/index.tsx index f233aca35..eaecd98a4 100644 --- a/apps/web/src/components/publish-view/index.tsx +++ b/apps/web/src/components/publish-view/index.tsx @@ -45,6 +45,7 @@ function PublishView(props: PublishViewProps) { ); const [isPasswordProtected, setIsPasswordProtected] = useState(false); const [selfDestruct, setSelfDestruct] = useState(false); + const [viewCount, setViewCount] = useState(); const [isPublishing, setIsPublishing] = useState(false); const [processingStatus, setProcessingStatus] = useState<{ total?: number; @@ -64,6 +65,7 @@ function PublishView(props: PublishViewProps) { setPublishId(monographId); setIsPasswordProtected(!!monograph.password); setSelfDestruct(!!monograph.selfDestruct); + setViewCount(monograph.viewCount); if (monograph.password) { const password = await db.monographs.decryptPassword( @@ -140,12 +142,38 @@ function PublishView(props: PublishViewProps) { overflow: "hidden" }} > - - {strings.publishedAt()} - + + {strings.publishedAt()} + + {typeof viewCount === "number" && ( + + {strings.views(viewCount)} + + )} + { datePublished: number; selfDestruct: boolean; password?: Cipher<"base64">; + viewCount?: number; } export type Match = { diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index 43a2b8f5d..1d6b9a566 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -96,6 +96,10 @@ msgstr "{count, plural, one {1 minute} other {# minutes}}" msgid "{count, plural, one {1 result} other {# results}}" msgstr "{count, plural, one {1 result} other {# results}}" +#: src/strings.ts:2610 +msgid "{count, plural, one {1 view} other {# views}}" +msgstr "{count, plural, one {1 view} other {# views}}" + #: generated/action-confirmations.ts:32 msgid "{count, plural, one {Are you sure you want to delete this attachment?} other {Are you sure you to delete these attachments?}}" msgstr "{count, plural, one {Are you sure you want to delete this attachment?} other {Are you sure you to delete these attachments?}}" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 2b257cbea..775af8496 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -96,6 +96,10 @@ msgstr "" msgid "{count, plural, one {1 result} other {# results}}" msgstr "" +#: src/strings.ts:2610 +msgid "{count, plural, one {1 view} other {# views}}" +msgstr "" + #: generated/action-confirmations.ts:32 msgid "{count, plural, one {Are you sure you want to delete this attachment?} other {Are you sure you to delete these attachments?}}" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index f9f9e0223..7f0dfb983 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2605,5 +2605,10 @@ Use this if changes from other devices are not appearing on this device. This wi clickToDirectlyClaimPromo: () => t`Click here to directly claim the promotion.`, loginToUploadAttachments: () => - t`Login to upload attachments. [Read more](https://help.notesnook.com/faqs/login-to-upload-attachments)` + t`Login to upload attachments. [Read more](https://help.notesnook.com/faqs/login-to-upload-attachments)`, + views: (count: number) => + plural(count, { + one: `1 view`, + other: `# views` + }) }; From 3e87e2ea5a296945fc4093e3e6929fbc72ab6815 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:22:52 +0500 Subject: [PATCH 2/8] web: use monograph stats api Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- .../web/src/components/publish-view/index.tsx | 22 ++++++++++++++----- packages/core/src/api/monographs.ts | 16 ++++++++++++++ packages/intl/locale/en.po | 4 ++++ packages/intl/locale/pseudo-LOCALE.po | 4 ++++ packages/intl/src/strings.ts | 3 ++- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/publish-view/index.tsx b/apps/web/src/components/publish-view/index.tsx index eaecd98a4..1d7a4762a 100644 --- a/apps/web/src/components/publish-view/index.tsx +++ b/apps/web/src/components/publish-view/index.tsx @@ -27,7 +27,7 @@ import { db } from "../../common/db"; import { writeText } from "clipboard-polyfill"; import { ScopedThemeProvider } from "../theme-provider"; import { showToast } from "../../utils/toast"; -import { EV, EVENTS, hosts } from "@notesnook/core"; +import { EV, EVENTS, hosts, MonographStats } from "@notesnook/core"; import { useStore } from "../../stores/monograph-store"; import ReactModal from "react-modal"; import { DialogButton } from "../dialog"; @@ -45,7 +45,7 @@ function PublishView(props: PublishViewProps) { ); const [isPasswordProtected, setIsPasswordProtected] = useState(false); const [selfDestruct, setSelfDestruct] = useState(false); - const [viewCount, setViewCount] = useState(); + const [stats, setStats] = useState(); const [isPublishing, setIsPublishing] = useState(false); const [processingStatus, setProcessingStatus] = useState<{ total?: number; @@ -65,7 +65,9 @@ function PublishView(props: PublishViewProps) { setPublishId(monographId); setIsPasswordProtected(!!monograph.password); setSelfDestruct(!!monograph.selfDestruct); - setViewCount(monograph.viewCount); + + const stats = await db.monographs.stats(monographId); + setStats(stats); if (monograph.password) { const password = await db.monographs.decryptPassword( @@ -155,7 +157,7 @@ function PublishView(props: PublishViewProps) { > {strings.publishedAt()} - {typeof viewCount === "number" && ( + {typeof stats?.viewCount === "number" && ( { + const stats = await db.monographs.stats(publishId); + setStats(stats); }} > - {strings.views(viewCount)} + {strings.views(stats.viewCount)} )} diff --git a/packages/core/src/api/monographs.ts b/packages/core/src/api/monographs.ts index f0f30dd21..79fe12a86 100644 --- a/packages/core/src/api/monographs.ts +++ b/packages/core/src/api/monographs.ts @@ -37,6 +37,9 @@ type EncryptedMonograph = MonographApiRequestBase & { type MonographApiRequest = (UnencryptedMonograph | EncryptedMonograph) & { userId: string; }; +export type MonographStats = { + viewCount: number; +}; export type PublishOptions = { password?: string; selfDestruct?: boolean }; export class Monographs { @@ -187,4 +190,17 @@ export class Monographs { if (!monographPasswordsKey) return ""; return this.db.storage().decrypt(monographPasswordsKey, password); } + + async stats(monographId: string) { + const token = await this.db.tokenManager.getAccessToken(); + const { viewCount } = (await http.get( + `${Constants.API_HOST}/monographs/${monographId}/stats`, + token + )) as MonographStats; + await this.db.monographsCollection.add({ + id: monographId, + viewCount + }); + return { viewCount }; + } } diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index 1d6b9a566..a5cdaf77a 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -1604,6 +1604,10 @@ msgstr "Click to remove" msgid "Click to reset {title}" msgstr "Click to reset {title}" +#: src/strings.ts:2614 +msgid "Click to update" +msgstr "Click to update" + #: src/strings.ts:1380 msgid "Close" msgstr "Close" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 775af8496..0a40a0372 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -1593,6 +1593,10 @@ msgstr "" msgid "Click to reset {title}" msgstr "" +#: src/strings.ts:2614 +msgid "Click to update" +msgstr "" + #: src/strings.ts:1380 msgid "Close" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index 7f0dfb983..6738ba398 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2610,5 +2610,6 @@ Use this if changes from other devices are not appearing on this device. This wi plural(count, { one: `1 view`, other: `# views` - }) + }), + clickToUpdate: () => t`Click to update` }; From 41ad8e098831ce8761bc99060f3b6db0485bb730 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Fri, 7 Nov 2025 19:26:32 +0500 Subject: [PATCH 3/8] web: improve publish view ux --- apps/web/src/components/editor/action-bar.tsx | 16 +- apps/web/src/components/note/index.tsx | 6 +- .../web/src/components/publish-view/index.tsx | 608 +++++++++--------- apps/web/src/components/toggle/index.jsx | 82 --- packages/intl/locale/en.po | 18 +- packages/intl/locale/pseudo-LOCALE.po | 18 +- packages/intl/src/strings.ts | 10 +- 7 files changed, 364 insertions(+), 394 deletions(-) delete mode 100644 apps/web/src/components/toggle/index.jsx diff --git a/apps/web/src/components/editor/action-bar.tsx b/apps/web/src/components/editor/action-bar.tsx index 56a247d8d..ed6f98965 100644 --- a/apps/web/src/components/editor/action-bar.tsx +++ b/apps/web/src/components/editor/action-bar.tsx @@ -91,7 +91,7 @@ type ToolButton = { hidden?: boolean; hideOnMobile?: boolean; toggled?: boolean; - onClick: () => void; + onClick: (e: React.MouseEvent) => void; }; export function EditorActionBar() { @@ -147,11 +147,15 @@ export function EditorActionBar() { enabled: activeSession && (activeSession.type === "default" || activeSession.type === "readonly"), - onClick: () => - activeSession && - (activeSession.type === "default" || - activeSession.type === "readonly") && - showPublishView(activeSession.note, "top") + onClick: (e) => { + if ( + !activeSession || + (activeSession.type !== "default" && + activeSession.type !== "readonly") + ) + return; + showPublishView(activeSession.note, e.target as HTMLElement); + } }, { title: strings.toc(), diff --git a/apps/web/src/components/note/index.tsx b/apps/web/src/components/note/index.tsx index 851872601..5eb5db14b 100644 --- a/apps/web/src/components/note/index.tsx +++ b/apps/web/src/components/note/index.tsx @@ -96,7 +96,7 @@ import { } from "../icons"; import { Context } from "../list-container/types"; import ListItem from "../list-item"; -import { showPublishView } from "../publish-view"; +import { PublishDialog } from "../publish-view"; import TimeAgo from "../time-ago"; type NoteProps = NoteResolvedData & { @@ -480,7 +480,7 @@ export const noteMenuItems: ( title: strings.update(), icon: Update.path, onClick: () => { - showPublishView(note, "bottom"); + PublishDialog.show({ note }); } }, { @@ -499,7 +499,7 @@ export const noteMenuItems: ( ] } : undefined, - onClick: () => showPublishView(note, "bottom") + onClick: () => PublishDialog.show({ note }) }, { type: "button", diff --git a/apps/web/src/components/publish-view/index.tsx b/apps/web/src/components/publish-view/index.tsx index 1d7a4762a..cdee1585c 100644 --- a/apps/web/src/components/publish-view/index.tsx +++ b/apps/web/src/components/publish-view/index.tsx @@ -18,35 +18,37 @@ along with this program. If not, see . */ import { useEffect, useRef, useState } from "react"; -import ReactDOM from "react-dom"; -import { Flex, Text, Button, Link } from "@theme-ui/components"; -import { Copy } from "../icons"; -import Toggle from "../toggle"; -import Field from "../field"; +import { Flex, Text, Button, Link, Switch, Input } from "@theme-ui/components"; +import { Loading, Refresh } from "../icons"; import { db } from "../../common/db"; import { writeText } from "clipboard-polyfill"; -import { ScopedThemeProvider } from "../theme-provider"; import { showToast } from "../../utils/toast"; -import { EV, EVENTS, hosts, MonographStats } from "@notesnook/core"; +import { EV, EVENTS, hosts, Monograph, MonographStats } from "@notesnook/core"; import { useStore } from "../../stores/monograph-store"; -import ReactModal from "react-modal"; -import { DialogButton } from "../dialog"; import { Note } from "@notesnook/core"; import { strings } from "@notesnook/intl"; +import { getFormattedDate, usePromise } from "@notesnook/common"; +import { createRoot, Root } from "react-dom/client"; +import { PopupPresenter } from "@notesnook/ui"; +import { BaseDialogProps, DialogManager } from "../../common/dialog-manager"; +import Dialog from "../../components/dialog"; type PublishViewProps = { note: Note; + monograph?: ResolvedMonograph; onClose: (result: boolean) => void; }; function PublishView(props: PublishViewProps) { const { note, onClose } = props; - const [publishId, setPublishId] = useState( - db.monographs.monograph(note.id) + const [selfDestruct, setSelfDestruct] = useState( + props.monograph?.selfDestruct ); - const [isPasswordProtected, setIsPasswordProtected] = useState(false); - const [selfDestruct, setSelfDestruct] = useState(false); - const [stats, setStats] = useState(); - const [isPublishing, setIsPublishing] = useState(false); + const [stats, setStats] = useState( + props.monograph?.stats || { viewCount: 0 } + ); + const [status, setStatus] = useState<{ + action: "publish" | "unpublish" | "stats"; + }>(); const [processingStatus, setProcessingStatus] = useState<{ total?: number; current: number; @@ -54,32 +56,7 @@ function PublishView(props: PublishViewProps) { const passwordInput = useRef(null); const publishNote = useStore((store) => store.publish); const unpublishNote = useStore((store) => store.unpublish); - - useEffect(() => { - if (!publishId) return; - (async () => { - const monographId = db.monographs.monograph(note.id); - if (monographId) { - const monograph = await db.monographs.get(monographId); - if (!monograph) return; - setPublishId(monographId); - setIsPasswordProtected(!!monograph.password); - setSelfDestruct(!!monograph.selfDestruct); - - const stats = await db.monographs.stats(monographId); - setStats(stats); - - if (monograph.password) { - const password = await db.monographs.decryptPassword( - monograph.password - ); - if (passwordInput.current) { - passwordInput.current.value = password; - } - } - } - })(); - }, [publishId, isPublishing, note.id]); + const [monograph, setMonograph] = useState(props.monograph); useEffect(() => { const fileDownloadedEvent = EV.subscribe( @@ -97,209 +74,161 @@ function PublishView(props: PublishViewProps) { }, [note.id]); return ( - - - + {monograph?.id ? ( + - {note.title} - - {isPublishing ? ( - - {strings.pleaseWait()}... - {processingStatus && ( - - {strings.downloadingImages()} ({processingStatus.current}/ - {processingStatus.total}) - - )} + {`${hosts.MONOGRAPH_HOST}/${monograph?.id}`} + + + + ) : null} + + {monograph?.publishedAt ? ( + + {strings.publishedAt()} + + {getFormattedDate(monograph?.publishedAt, "date-time")} + - ) : ( - <> - {publishId ? ( - - - - {strings.publishedAt()} - - {typeof stats?.viewCount === "number" && ( - { - const stats = await db.monographs.stats(publishId); - setStats(stats); - }} - > - {strings.views(stats.viewCount)} - - )} - - - - {`${hosts.MONOGRAPH_HOST}/${publishId}`} - - - - - ) : ( - - {strings.monographDesc()} + ) : null} + {monograph?.id ? ( + + {strings.views()} + + + {stats.viewCount} - )} - setSelfDestruct((s) => !s)} - /> - setIsPasswordProtected((s) => !s)} - /> - {isPasswordProtected && ( - - )} - - )} + + + + ) : null} + setSelfDestruct((s) => !s)} + title={strings.monographSelfDestructDesc()} + > + {strings.monographSelfDestructHeading()} + e.stopPropagation()} + /> + + + + {strings.monographPassHeading()} + + - { - try { - setIsPublishing(true); - const password = passwordInput.current?.value; - - const publishId = await publishNote(note.id, { - selfDestruct, - password - }); - setPublishId(publishId); - onClose(true); - showToast("success", strings.actions.published.note(1)); - } catch (e) { - console.error(e); - showToast( - "error", - `${strings.actionErrors.published.note(1)}: ${ - (e as Error).message - }` - ); - } finally { - setIsPublishing(false); - } - }} - loading={isPublishing} - text={publishId ? strings.update() : strings.publish()} - /> - {publishId && ( - { try { - setIsPublishing(true); + setStatus({ action: "unpublish" }); await unpublishNote(note.id); - setPublishId(undefined); onClose(true); showToast("success", strings.actions.unpublished.note(1)); } catch (e) { @@ -310,74 +239,179 @@ function PublishView(props: PublishViewProps) { (e as Error).message ); } finally { - setIsPublishing(false); + setStatus(undefined); } }} - text={"Unpublish"} - /> + sx={{ textAlign: "left", borderRadius: "none" }} + disabled={!!status} + > + {status?.action === "unpublish" ? ( + + ) : ( + strings.unpublish() + )} + )} + - + {processingStatus ? ( + + {strings.downloadingImages()} {processingStatus.current}/ + {processingStatus.total || "?"} + + ) : null} + ); } export default PublishView; -export function showPublishView(note: Note, location = "top") { - const root = document.getElementById("dialogContainer"); +let root: Root | null = null; - if (root) { - return new Promise((resolve) => { - const perform = (result: boolean) => { - ReactDOM.unmountComponentAtNode(root); - closePublishView(); - resolve(result); - }; - ReactDOM.render( - perform(false)} - preventScroll={false} - shouldCloseOnOverlayClick - shouldCloseOnEsc - shouldFocusAfterRender - shouldReturnFocusAfterClose - style={{ - overlay: { backgroundColor: "transparent", zIndex: 999 }, - content: { - padding: 0, - top: location === "top" ? 60 : undefined, - right: location === "top" ? 10 : undefined, - bottom: location === "bottom" ? 0 : undefined, - left: location === "bottom" ? 0 : undefined, - background: "transparent", - border: "none", - borderRadius: 0, - boxShadow: "0px 0px 15px 0px #00000011" - } - }} - > - - , - root - ); - }); - } - return Promise.reject("No element with id 'dialogContainer'"); +function close() { + root?.unmount(); + root = null; } -function closePublishView() { - const root = document.getElementById("dialogContainer"); - if (root) { - root.innerHTML = ""; - } +export async function showPublishView(note: Note, target?: HTMLElement) { + const rootElement = document.getElementById("dialogContainer"); + if (!rootElement) return; + + if (root) return close(); + + const monograph = await resolveMonograph(note.id); + + root = createRoot(rootElement); + root.render( + close()} + position={{ + target, + location: "below", + isTargetAbsolute: true, + yOffset: 10, + xOffset: -10 + }} + sx={{ + boxShadow: "0px 0px 15px 0px #00000011" + }} + scope="dialog" + > + + {strings.publishToTheWeb()} + {strings.monographDesc()} + close()} + /> + + + ); +} + +export const PublishDialog = DialogManager.register(function PublishDialog( + props: BaseDialogProps & { note: Note } +) { + const monograph = usePromise( + () => resolveMonograph(props.note.id), + [props.note.id] + ); + + if (monograph.status !== "fulfilled") return null; + return ( + props.onClose(false)} + > + + + + + ); +}); + +type ResolvedMonograph = { + id: string; + selfDestruct: boolean; + publishedAt?: number; + stats: MonographStats; + password?: string; +}; + +async function resolveMonograph( + monographId: string +): Promise { + const monograph = await db.monographs.get(monographId); + if (!monograph) return; + return { + id: monographId, + selfDestruct: !!monograph.selfDestruct, + publishedAt: monograph.datePublished, + stats: await db.monographs.stats(monographId), + password: monograph.password + ? await db.monographs.decryptPassword(monograph.password) + : undefined + }; } diff --git a/apps/web/src/components/toggle/index.jsx b/apps/web/src/components/toggle/index.jsx deleted file mode 100644 index d46134ae9..000000000 --- a/apps/web/src/components/toggle/index.jsx +++ /dev/null @@ -1,82 +0,0 @@ -/* -This file is part of the Notesnook project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -import { useCallback, useState } from "react"; -import Tip from "../tip"; -import { Flex, Switch } from "@theme-ui/components"; -import { Loading } from "../icons"; - -function Toggle(props) { - const { - title, - onTip, - offTip, - isToggled, - onToggled, - onlyIf, - testId, - disabled, - tip - } = props; - const [isLoading, setIsLoading] = useState(false); - const onClick = useCallback(async () => { - setIsLoading(true); - try { - await onToggled(); - } finally { - setIsLoading(false); - } - }, [onToggled]); - - if (onlyIf === false) return null; - return ( - label": { width: "auto", flexShrink: 0 } - }} - > - - {isLoading ? ( - - ) : ( - - )} - - ); -} -export default Toggle; diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index a5cdaf77a..9cab86210 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -96,10 +96,6 @@ msgstr "{count, plural, one {1 minute} other {# minutes}}" msgid "{count, plural, one {1 result} other {# results}}" msgstr "{count, plural, one {1 result} other {# results}}" -#: src/strings.ts:2610 -msgid "{count, plural, one {1 view} other {# views}}" -msgstr "{count, plural, one {1 view} other {# views}}" - #: generated/action-confirmations.ts:32 msgid "{count, plural, one {Are you sure you want to delete this attachment?} other {Are you sure you to delete these attachments?}}" msgstr "{count, plural, one {Are you sure you want to delete this attachment?} other {Are you sure you to delete these attachments?}}" @@ -1604,7 +1600,7 @@ msgstr "Click to remove" msgid "Click to reset {title}" msgstr "Click to reset {title}" -#: src/strings.ts:2614 +#: src/strings.ts:2610 msgid "Click to update" msgstr "Click to update" @@ -4108,6 +4104,10 @@ msgstr "No notebooks selected to move" msgid "No one can view this {type} except you." msgstr "No one can view this {type} except you." +#: src/strings.ts:2611 +msgid "No password" +msgstr "No password" + #: src/strings.ts:279 msgid "No references found of this note" msgstr "No references found of this note" @@ -4878,6 +4878,10 @@ msgstr "Publish" msgid "Publish note" msgstr "Publish note" +#: src/strings.ts:2612 +msgid "Publish to the web" +msgstr "Publish to the web" + #: src/strings.ts:488 msgid "Publish your note to share it with others. You can set a password to protect it." msgstr "Publish your note to share it with others. You can set a password to protect it." @@ -7038,6 +7042,10 @@ msgstr "View source code" msgid "View your recovery codes to recover your account in case you lose access to your two-factor authentication methods." msgstr "View your recovery codes to recover your account in case you lose access to your two-factor authentication methods." +#: src/strings.ts:2609 +msgid "Views" +msgstr "Views" + #: src/strings.ts:416 msgid "Visit homepage" msgstr "Visit homepage" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 0a40a0372..4b597f800 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -96,10 +96,6 @@ msgstr "" msgid "{count, plural, one {1 result} other {# results}}" msgstr "" -#: src/strings.ts:2610 -msgid "{count, plural, one {1 view} other {# views}}" -msgstr "" - #: generated/action-confirmations.ts:32 msgid "{count, plural, one {Are you sure you want to delete this attachment?} other {Are you sure you to delete these attachments?}}" msgstr "" @@ -1593,7 +1589,7 @@ msgstr "" msgid "Click to reset {title}" msgstr "" -#: src/strings.ts:2614 +#: src/strings.ts:2610 msgid "Click to update" msgstr "" @@ -4088,6 +4084,10 @@ msgstr "" msgid "No one can view this {type} except you." msgstr "" +#: src/strings.ts:2611 +msgid "No password" +msgstr "" + #: src/strings.ts:279 msgid "No references found of this note" msgstr "" @@ -4852,6 +4852,10 @@ msgstr "" msgid "Publish note" msgstr "" +#: src/strings.ts:2612 +msgid "Publish to the web" +msgstr "" + #: src/strings.ts:488 msgid "Publish your note to share it with others. You can set a password to protect it." msgstr "" @@ -6989,6 +6993,10 @@ msgstr "" msgid "View your recovery codes to recover your account in case you lose access to your two-factor authentication methods." msgstr "" +#: src/strings.ts:2609 +msgid "Views" +msgstr "" + #: src/strings.ts:416 msgid "Visit homepage" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index 6738ba398..f71e13b4d 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2606,10 +2606,8 @@ Use this if changes from other devices are not appearing on this device. This wi t`Click here to directly claim the promotion.`, loginToUploadAttachments: () => t`Login to upload attachments. [Read more](https://help.notesnook.com/faqs/login-to-upload-attachments)`, - views: (count: number) => - plural(count, { - one: `1 view`, - other: `# views` - }), - clickToUpdate: () => t`Click to update` + views: () => t`Views`, + clickToUpdate: () => t`Click to update`, + noPassword: () => t`No password`, + publishToTheWeb: () => t`Publish to the web` }; From 71952a354ea642ed32fa8ba26ce246c5cf4607b6 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 8 Nov 2025 12:46:58 +0500 Subject: [PATCH 4/8] core: monographs stats -> monograph analytics --- packages/core/src/api/monographs.ts | 18 +++++++----------- packages/core/src/collections/monographs.ts | 1 - packages/core/src/database/migrations.ts | 8 -------- packages/core/src/types.ts | 1 - 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/core/src/api/monographs.ts b/packages/core/src/api/monographs.ts index 79fe12a86..d388bb724 100644 --- a/packages/core/src/api/monographs.ts +++ b/packages/core/src/api/monographs.ts @@ -37,8 +37,8 @@ type EncryptedMonograph = MonographApiRequestBase & { type MonographApiRequest = (UnencryptedMonograph | EncryptedMonograph) & { userId: string; }; -export type MonographStats = { - viewCount: number; +export type MonographAnalytics = { + totalViews: number; }; export type PublishOptions = { password?: string; selfDestruct?: boolean }; @@ -191,16 +191,12 @@ export class Monographs { return this.db.storage().decrypt(monographPasswordsKey, password); } - async stats(monographId: string) { + async analytics(monographId: string) { const token = await this.db.tokenManager.getAccessToken(); - const { viewCount } = (await http.get( - `${Constants.API_HOST}/monographs/${monographId}/stats`, + const analytics = (await http.get( + `${Constants.API_HOST}/monographs/${monographId}/analytics`, token - )) as MonographStats; - await this.db.monographsCollection.add({ - id: monographId, - viewCount - }); - return { viewCount }; + )) as MonographAnalytics; + return analytics; } } diff --git a/packages/core/src/collections/monographs.ts b/packages/core/src/collections/monographs.ts index 7c92d4f19..c9d0fd439 100644 --- a/packages/core/src/collections/monographs.ts +++ b/packages/core/src/collections/monographs.ts @@ -62,7 +62,6 @@ export class Monographs implements ICollection { datePublished: merged.datePublished, selfDestruct: merged.selfDestruct, password: merged.password, - viewCount: merged.viewCount || 0, type: "monograph" }); } diff --git a/packages/core/src/database/migrations.ts b/packages/core/src/database/migrations.ts index 96610d36e..afbdd1e64 100644 --- a/packages/core/src/database/migrations.ts +++ b/packages/core/src/database/migrations.ts @@ -404,14 +404,6 @@ export class NNMigrationProvider implements MigrationProvider { .addColumn("password", "text") .execute(); } - }, - "a-2025-11-05": { - async up(db) { - await db.schema - .alterTable("monographs") - .addColumn("viewCount", "integer") - .execute(); - } } }; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c8c5f2c2c..5b3403b76 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -498,7 +498,6 @@ export interface Monograph extends BaseItem<"monograph"> { datePublished: number; selfDestruct: boolean; password?: Cipher<"base64">; - viewCount?: number; } export type Match = { From 28b563b95b06f85386a82e5e8d7215514f70b79f Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 8 Nov 2025 12:49:36 +0500 Subject: [PATCH 5/8] web: make monograph analytics a pro feature --- .../web/src/components/publish-view/index.tsx | 70 +++++++++++++------ .../common/src/utils/is-feature-available.ts | 11 +++ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/apps/web/src/components/publish-view/index.tsx b/apps/web/src/components/publish-view/index.tsx index cdee1585c..5445aa0f6 100644 --- a/apps/web/src/components/publish-view/index.tsx +++ b/apps/web/src/components/publish-view/index.tsx @@ -23,15 +23,20 @@ import { Loading, Refresh } from "../icons"; import { db } from "../../common/db"; import { writeText } from "clipboard-polyfill"; import { showToast } from "../../utils/toast"; -import { EV, EVENTS, hosts, Monograph, MonographStats } from "@notesnook/core"; +import { EV, EVENTS, hosts, MonographAnalytics } from "@notesnook/core"; import { useStore } from "../../stores/monograph-store"; import { Note } from "@notesnook/core"; import { strings } from "@notesnook/intl"; -import { getFormattedDate, usePromise } from "@notesnook/common"; +import { + getFormattedDate, + useIsFeatureAvailable, + usePromise +} from "@notesnook/common"; import { createRoot, Root } from "react-dom/client"; import { PopupPresenter } from "@notesnook/ui"; import { BaseDialogProps, DialogManager } from "../../common/dialog-manager"; import Dialog from "../../components/dialog"; +import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog"; type PublishViewProps = { note: Note; @@ -43,11 +48,11 @@ function PublishView(props: PublishViewProps) { const [selfDestruct, setSelfDestruct] = useState( props.monograph?.selfDestruct ); - const [stats, setStats] = useState( - props.monograph?.stats || { viewCount: 0 } + const [analytics, setAnalytics] = useState( + props.monograph?.analytics || { totalViews: 0 } ); const [status, setStatus] = useState<{ - action: "publish" | "unpublish" | "stats"; + action: "publish" | "unpublish" | "analytics"; }>(); const [processingStatus, setProcessingStatus] = useState<{ total?: number; @@ -57,6 +62,7 @@ function PublishView(props: PublishViewProps) { const publishNote = useStore((store) => store.publish); const unpublishNote = useStore((store) => store.unpublish); const [monograph, setMonograph] = useState(props.monograph); + const monographAnalytics = useIsFeatureAvailable("monographAnalytics"); useEffect(() => { const fileDownloadedEvent = EV.subscribe( @@ -144,25 +150,43 @@ function PublishView(props: PublishViewProps) { }} > {strings.views()} - - - {stats.viewCount} - + {monographAnalytics?.isAllowed ? ( + + + {analytics.totalViews} + + + + ) : monographAnalytics ? ( - + ) : null} ) : null} Date: Sat, 8 Nov 2025 12:59:17 +0500 Subject: [PATCH 6/8] core: handle errors on monograph analytics fetch --- packages/core/src/api/monographs.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/core/src/api/monographs.ts b/packages/core/src/api/monographs.ts index d388bb724..53e11cc10 100644 --- a/packages/core/src/api/monographs.ts +++ b/packages/core/src/api/monographs.ts @@ -191,12 +191,16 @@ export class Monographs { return this.db.storage().decrypt(monographPasswordsKey, password); } - async analytics(monographId: string) { - const token = await this.db.tokenManager.getAccessToken(); - const analytics = (await http.get( - `${Constants.API_HOST}/monographs/${monographId}/analytics`, - token - )) as MonographAnalytics; - return analytics; + async analytics(monographId: string): Promise { + try { + const token = await this.db.tokenManager.getAccessToken(); + const analytics = (await http.get( + `${Constants.API_HOST}/monographs/${monographId}/analytics`, + token + )) as MonographAnalytics; + return analytics; + } catch { + return { totalViews: 0 }; + } } } From 69a3fa4d90b7dd8551bd87a10484557d81a035d1 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 8 Nov 2025 12:59:29 +0500 Subject: [PATCH 7/8] web: lazily load monograph analytics --- .../web/src/components/publish-view/index.tsx | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/apps/web/src/components/publish-view/index.tsx b/apps/web/src/components/publish-view/index.tsx index 5445aa0f6..af2404110 100644 --- a/apps/web/src/components/publish-view/index.tsx +++ b/apps/web/src/components/publish-view/index.tsx @@ -48,9 +48,6 @@ function PublishView(props: PublishViewProps) { const [selfDestruct, setSelfDestruct] = useState( props.monograph?.selfDestruct ); - const [analytics, setAnalytics] = useState( - props.monograph?.analytics || { totalViews: 0 } - ); const [status, setStatus] = useState<{ action: "publish" | "unpublish" | "analytics"; }>(); @@ -63,6 +60,10 @@ function PublishView(props: PublishViewProps) { const unpublishNote = useStore((store) => store.unpublish); const [monograph, setMonograph] = useState(props.monograph); const monographAnalytics = useIsFeatureAvailable("monographAnalytics"); + const analytics = usePromise(async () => { + if (!monographAnalytics?.isAllowed || !monograph) return { totalViews: 0 }; + return await db.monographs.analytics(monograph?.id); + }, [monograph?.id, monographAnalytics]); useEffect(() => { const fileDownloadedEvent = EV.subscribe( @@ -151,32 +152,36 @@ function PublishView(props: PublishViewProps) { > {strings.views()} {monographAnalytics?.isAllowed ? ( - - - {analytics.totalViews} - - - + analytics.status === "fulfilled" ? ( + + + {analytics.value.totalViews} + + + + ) : ( + + ) ) : monographAnalytics ? (