From 2c078de2c0957a452df6cccbc731faff037296c1 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Sat, 10 Jan 2026 11:01:51 +0500 Subject: [PATCH 01/10] core: save note title in note session history item --- packages/core/__tests__/note-history.test.js | 40 +++++++++++++++++++ packages/core/src/collections/note-history.ts | 14 +++++-- packages/core/src/collections/notes.ts | 7 ++++ .../core/src/collections/session-content.ts | 36 ++++++++++++----- packages/core/src/database/migrations.ts | 9 ++++- packages/core/src/types.ts | 1 + 6 files changed, 93 insertions(+), 14 deletions(-) diff --git a/packages/core/__tests__/note-history.test.js b/packages/core/__tests__/note-history.test.js index c53ac86be..66f3ed062 100644 --- a/packages/core/__tests__/note-history.test.js +++ b/packages/core/__tests__/note-history.test.js @@ -199,3 +199,43 @@ test("locking an old note should clear its history", () => expect(await db.noteHistory.get(id).count()).toBe(0); } )); + +test("note history item can be created by setting note title", () => + noteTest({ title: "Test note", sessionId: "notesession" }).then( + async ({ db, id }) => { + expect(await db.noteHistory.get(id).count()).toBe(1); + const history = await db.noteHistory.get(id); + const items = await history.items(); + const content = await db.noteHistory.sessionContent.get( + items[0].sessionContentId + ); + expect(content.title).toBe("Test note"); + } + )); + +test("note history item can be created by setting note title and content both", () => + noteTest({ title: "Test note", ...TEST_NOTE, sessionId: "notesession" }).then( + async ({ db, id }) => { + expect(await db.noteHistory.get(id).count()).toBe(1); + const history = db.noteHistory.get(id); + const items = await history.items(); + const content = await db.noteHistory.sessionContent.get( + items[0].sessionContentId + ); + expect(await db.noteHistory.sessionContent.collection.count()).toBe(1); + expect(content.data).toBe(TEST_NOTE.content.data); + expect(content.title).toBe("Test note"); + await db.notes.add({ + id: id, + content: TEST_NOTE.content + }); + expect(content.data).toBe(TEST_NOTE.content.data); + expect(content.title).toBe("Test note"); + await db.notes.add({ + id: id, + title: "Test note" + }); + expect(content.data).toBe(TEST_NOTE.content.data); + expect(content.title).toBe("Test note"); + } + )); diff --git a/packages/core/src/collections/note-history.ts b/packages/core/src/collections/note-history.ts index f56da3afb..bd8275baa 100644 --- a/packages/core/src/collections/note-history.ts +++ b/packages/core/src/collections/note-history.ts @@ -74,13 +74,19 @@ export class NoteHistory implements ICollection { async add( sessionId: string, - content: NoteContent & { noteId: string; locked: boolean } + content: Partial> & { + noteId: string; + locked?: boolean; + title?: string; + } ) { const { noteId, locked } = content; sessionId = `${noteId}_${sessionId}`; if (await this.collection.exists(sessionId)) { - await this.collection.update([sessionId], { locked }); + await this.collection.update([sessionId], { + locked + }); } else { await this.collection.upsert({ type: "session", @@ -93,7 +99,9 @@ export class NoteHistory implements ICollection { locked }); } + await this.sessionContent.add(sessionId, content, locked); + await this.cleanup(noteId); return sessionId; @@ -189,7 +197,7 @@ export class NoteHistory implements ICollection { data: content.data, type: content.type }); - } else if (content.data && !isCipher(content.data)) { + } else if (content.data && content.type && !isCipher(content.data)) { await this.db.notes.add({ id: session.noteId, sessionId: `${Date.now()}`, diff --git a/packages/core/src/collections/notes.ts b/packages/core/src/collections/notes.ts index 03085d14c..7d3364f4d 100644 --- a/packages/core/src/collections/notes.ts +++ b/packages/core/src/collections/notes.ts @@ -205,6 +205,13 @@ export class Notes implements ICollection { }); this.totalNotes++; } + + if (item.sessionId) { + await this.db.noteHistory.add(item.sessionId, { + title: item.title, + noteId: id + }); + } }); return id; } diff --git a/packages/core/src/collections/session-content.ts b/packages/core/src/collections/session-content.ts index 9f41de393..1931b0f7f 100644 --- a/packages/core/src/collections/session-content.ts +++ b/packages/core/src/collections/session-content.ts @@ -44,30 +44,45 @@ export class SessionContent implements ICollection { async add( sessionId: string, - content: NoteContent, - locked: TLocked + content: Partial> & { title?: string }, + locked?: TLocked ) { if (!sessionId || !content) return; // const data = // locked || isCipher(content.data) // ? content.data // : await this.db.compressor().compress(content.data); - await this.collection.upsert({ + + const sessionContentItemId = makeSessionContentId(sessionId); + const sessionItem: Partial = { type: "sessioncontent", - id: makeSessionContentId(sessionId), - data: content.data, - contentType: content.type, + id: sessionContentItemId, compressed: false, localOnly: true, - locked, + locked: locked || false, dateCreated: Date.now(), dateModified: Date.now() - }); + }; + + if (content.data && content.type) { + sessionItem.data = content.data; + sessionItem.contentType = content.type; + } + + if (content.title) { + sessionItem.title = content.title; + } + + if (await this.collection.exists(sessionContentItemId)) { + this.collection.update([sessionContentItemId], sessionItem); + } else { + await this.collection.upsert(sessionItem as SessionContentItem); + } } async get( sessionContentId: string - ): Promise | undefined> { + ): Promise & { title: string }> | undefined> { const session = await this.collection.get(sessionContentId); if (!session || isDeleted(session)) return; @@ -90,7 +105,8 @@ export class SessionContent implements ICollection { session.compressed && !isCipher(session.data) ? await compressor.decompress(session.data) : session.data, - type: session.contentType + type: session.contentType, + title: session.title }; } diff --git a/packages/core/src/database/migrations.ts b/packages/core/src/database/migrations.ts index 9abf52e73..738fd6fe0 100644 --- a/packages/core/src/database/migrations.ts +++ b/packages/core/src/database/migrations.ts @@ -414,7 +414,14 @@ export class NNMigrationProvider implements MigrationProvider { await db.schema .createIndex("note_expiry_date") .on("notes") - .expression(sql`expiryDate ->> '$.value'`) + .expression(sql`expiryDate ->> '$.value'`); + } + }, + "a-2026-01-09": { + async up(db) { + await db.schema + .alterTable("sessioncontent") + .addColumn("title", "text") .execute(); } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6ea861fbe..75c6bb0a3 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -433,6 +433,7 @@ export interface SessionContentItem extends BaseItem<"sessioncontent"> { compressed: boolean; localOnly: boolean; locked: boolean; + title: string; } export type TrashCleanupInterval = 1 | 7 | 30 | 365 | -1; From 1e023500be29bd811b7a92105a5d76d0d75ccc9a Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Sat, 10 Jan 2026 12:55:03 +0500 Subject: [PATCH 02/10] mobile: add support for setting note history session title --- .../app/components/note-history/index.tsx | 5 +- .../note-history/{preview.jsx => preview.tsx} | 85 ++++++++++++++----- .../app/screens/editor/tiptap/use-editor.ts | 5 +- 3 files changed, 69 insertions(+), 26 deletions(-) rename apps/mobile/app/components/note-history/{preview.jsx => preview.tsx} (72%) diff --git a/apps/mobile/app/components/note-history/index.tsx b/apps/mobile/app/components/note-history/index.tsx index a369975db..a39cd11b8 100644 --- a/apps/mobile/app/components/note-history/index.tsx +++ b/apps/mobile/app/components/note-history/index.tsx @@ -47,7 +47,7 @@ const HistoryItem = ({ }: { index: number; items?: VirtualizedGrouping; - note?: Note; + note: Note; }) => { const [item] = useDBItem(index, "noteHistory", items); const { colors } = useThemeColors(); @@ -73,6 +73,7 @@ const HistoryItem = ({ session: getDate(item.dateCreated, item.dateModified) }} content={content} + note={note} /> ), context: "note_history" @@ -135,7 +136,7 @@ export default function NoteHistory({ const renderItem = useCallback( ({ index }: { index: number }) => ( - + ), [history] ); diff --git a/apps/mobile/app/components/note-history/preview.jsx b/apps/mobile/app/components/note-history/preview.tsx similarity index 72% rename from apps/mobile/app/components/note-history/preview.jsx rename to apps/mobile/app/components/note-history/preview.tsx index 65dc0ba0f..c61f972c3 100644 --- a/apps/mobile/app/components/note-history/preview.jsx +++ b/apps/mobile/app/components/note-history/preview.tsx @@ -37,21 +37,43 @@ import Paragraph from "../ui/typography/paragraph"; import { diff } from "diffblazer"; import { strings } from "@notesnook/intl"; import { DefaultAppStyles } from "../../utils/styles"; +import { + HistorySession, + isEncryptedContent, + Note, + NoteContent, + SessionContentItem, + TrashOrItem +} from "@notesnook/core"; /** * * @param {any} param0 * @returns */ -export default function NotePreview({ session, content, note }) { +export default function NotePreview({ + session, + content, + note +}: { + session: HistorySession & { session: string }; + content: + | Partial< + NoteContent & { + title: string; + } + > + | undefined; + note: TrashOrItem; +}) { const { colors } = useThemeColors(); const [locked, setLocked] = useState(false); async function restore() { if (note && note.type === "trash") { - if ((await db.trash.restore(note.id)) === false) return; + await db.trash.restore(note.id); Navigation.queueRoutesForUpdate(); - useSelectionStore.getState().setSelectionMode(false); + useSelectionStore.getState().setSelectionMode(); ToastManager.show({ heading: strings.noteRestored(), type: "success" @@ -91,15 +113,17 @@ export default function NotePreview({ session, content, note }) { negativeText: strings.cancel(), context: "local", positivePress: async () => { - await db.trash.delete(note.id); - useTrashStore.getState().refresh(); - useSelectionStore.getState().setSelectionMode(false); - ToastManager.show({ - heading: strings.noteDeleted(), - type: "success", - context: "local" - }); - eSendEvent(eCloseSheet); + if (note) { + await db.trash.delete(note.id); + useTrashStore.getState().refresh(); + useSelectionStore.getState().setSelectionMode(); + ToastManager.show({ + heading: strings.noteDeleted(), + type: "success", + context: "local" + }); + eSendEvent(eCloseSheet); + } }, positiveType: "error" }); @@ -113,8 +137,11 @@ export default function NotePreview({ session, content, note }) { }} > - - {!session?.locked && !locked ? ( + + {!session?.locked && !locked && content?.data ? ( { try { - if (content.data) { - const _note = note || (await db.notes.note(session?.noteId)); - const currentContent = await db.content.get(_note.contentId); - loadContent({ - data: diff(currentContent.data, content.data), - id: _note.id - }); + if (content?.data) { + const currentContent = note?.contentId + ? await db.content.get(note.contentId) + : undefined; + + if ( + currentContent?.data && + !isEncryptedContent(currentContent) + ) { + loadContent({ + data: diff( + currentContent?.data || "

", + content.data as string + ), + id: session?.noteId + }); + } } } catch (e) { ToastManager.error( - e, + e as Error, "Failed to load history preview", "local" ); @@ -153,7 +190,9 @@ export default function NotePreview({ session, content, note }) { }} > - {strings.encryptedNoteHistoryNotice()} + {!content?.data + ? strings.noContent() + : strings.encryptedNoteHistoryNotice()}
)} diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor.ts b/apps/mobile/app/screens/editor/tiptap/use-editor.ts index 478b7dfeb..a2123fbfa 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-editor.ts @@ -298,7 +298,8 @@ export const useEditor = ( return; } - if (isContentInvalid(data) && id) { + if (!title && isContentInvalid(data) && id) { + console.log("new editor session", title, isContentInvalid(data), id); // Create a new history session if recieved empty or invalid content // To ensure that history is preserved for correct content. currentSessionHistoryId = editorSessionHistory.newSession(id); @@ -312,6 +313,8 @@ export const useEditor = ( sessionId: `${currentSessionHistoryId}` }; + console.log(noteData.sessionId); + noteData.title = title; if (ignoreEdit) { From e6eb71a484b3f7e008b61b7ae6b4f888429fdb83 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Sat, 10 Jan 2026 12:55:21 +0500 Subject: [PATCH 03/10] core: restoring note history with title --- packages/core/__tests__/note-history.test.js | 100 ++++++++++++++---- packages/core/src/collections/note-history.ts | 42 ++++++-- packages/intl/locale/en.po | 4 + packages/intl/locale/pseudo-LOCALE.po | 4 + packages/intl/src/strings.ts | 3 +- 5 files changed, 125 insertions(+), 28 deletions(-) diff --git a/packages/core/__tests__/note-history.test.js b/packages/core/__tests__/note-history.test.js index 66f3ed062..2e8e805e4 100644 --- a/packages/core/__tests__/note-history.test.js +++ b/packages/core/__tests__/note-history.test.js @@ -214,28 +214,90 @@ test("note history item can be created by setting note title", () => )); test("note history item can be created by setting note title and content both", () => - noteTest({ title: "Test note", ...TEST_NOTE, sessionId: "notesession" }).then( + noteTest({ + ...TEST_NOTE, + title: "Test note", + sessionId: "notesession" + }).then(async ({ db, id }) => { + expect(await db.noteHistory.get(id).count()).toBe(1); + const history = db.noteHistory.get(id); + const items = await history.items(); + const content = await db.noteHistory.sessionContent.get( + items[0].sessionContentId + ); + expect(await db.noteHistory.collection.count()).toBe(1); + expect(content.data).toBe(TEST_NOTE.content.data); + expect(content.title).toBe("Test note"); + await db.notes.add({ + id: id, + content: TEST_NOTE.content, + sessionId: "notesession" + }); + expect(content.data).toBe(TEST_NOTE.content.data); + expect(content.title).toBe("Test note"); + await db.notes.add({ + id: id, + title: "Test note", + sessionId: "notesession" + }); + expect(content.data).toBe(TEST_NOTE.content.data); + expect(content.title).toBe("Test note"); + })); + +test("restoring an old session should replace note's content title", () => + noteTest({ title: "Test note", sessionId: Date.now() }).then( async ({ db, id }) => { - expect(await db.noteHistory.get(id).count()).toBe(1); - const history = db.noteHistory.get(id); - const items = await history.items(); - const content = await db.noteHistory.sessionContent.get( - items[0].sessionContentId - ); - expect(await db.noteHistory.sessionContent.collection.count()).toBe(1); - expect(content.data).toBe(TEST_NOTE.content.data); - expect(content.title).toBe("Test note"); + await delay(1000); + let newTitle = "Test note (edited)"; + + const sessionId = `${Date.now() + 10000}`; await db.notes.add({ id: id, - content: TEST_NOTE.content + title: newTitle, + sessionId }); - expect(content.data).toBe(TEST_NOTE.content.data); - expect(content.title).toBe("Test note"); - await db.notes.add({ - id: id, - title: "Test note" - }); - expect(content.data).toBe(TEST_NOTE.content.data); - expect(content.title).toBe("Test note"); + + const [, firstVersion] = await db.noteHistory + .get(id) + .items(undefined, { sortBy: "dateModified", sortDirection: "desc" }); + expect(firstVersion.id).not.toBe(`${id}_${sessionId}`); + await db.noteHistory.restore(firstVersion.id); + + const title = (await db.notes.note(id)).title; + expect(title).toBe("Test note"); + } + )); + +test("restoring an old session should replace note's content and title", () => + noteTest({ ...TEST_NOTE, title: "Test note", sessionId: Date.now() }).then( + async ({ db, id }) => { + await delay(1000); + let newTitle = "Test note (edited)"; + let editedContent = { + data: TEST_NOTE.content.data + "

Some new content

", + type: "tiptap" + }; + + const sessionId = `${Date.now() + 10000}`; + await db.notes.add({ + id: id, + title: newTitle, + sessionId, + content: editedContent + }); + + const [, firstVersion] = await db.noteHistory + .get(id) + .items(undefined, { sortBy: "dateModified", sortDirection: "desc" }); + expect(firstVersion.id).not.toBe(`${id}_${sessionId}`); + await db.noteHistory.restore(firstVersion.id); + + const title = (await db.notes.note(id)).title; + expect(title).toBe("Test note"); + + const contentId = (await db.notes.note(id)).contentId; + expect((await db.content.get(contentId)).data).toBe( + TEST_NOTE.content.data + ); } )); diff --git a/packages/core/src/collections/note-history.ts b/packages/core/src/collections/note-history.ts index bd8275baa..0ebff07c4 100644 --- a/packages/core/src/collections/note-history.ts +++ b/packages/core/src/collections/note-history.ts @@ -20,7 +20,7 @@ along with this program. If not, see . import Database from "../api/index.js"; import { isCipher } from "../utils/crypto.js"; import { FilteredSelector, SQLCollection } from "../database/sql-collection.js"; -import { HistorySession, isDeleted, NoteContent } from "../types.js"; +import { HistorySession, isDeleted, Note, NoteContent } from "../types.js"; import { makeSessionContentId } from "../utils/id.js"; import { ICollection } from "./collection.js"; import { SessionContent } from "./session-content.js"; @@ -190,22 +190,48 @@ export class NoteHistory implements ICollection { if (!note || !content) return; if (session.locked && isCipher(content.data)) { + const sessionId = `${Date.now()}`; await this.db.content.add({ id: note.contentId, noteId: session.noteId, - sessionId: `${Date.now()}`, + sessionId: sessionId, data: content.data, type: content.type }); - } else if (content.data && content.type && !isCipher(content.data)) { - await this.db.notes.add({ + + if (content.title) { + await this.db.notes.add({ + id: session.noteId, + sessionId: sessionId, + title: content.title + }); + } + } else if ( + (content.data && content.type && !isCipher(content.data)) || + content.title + ) { + const note: Partial< + Note & { + content: NoteContent; + sessionId: string; + } + > = { id: session.noteId, - sessionId: `${Date.now()}`, - content: { + sessionId: `${Date.now()}` + }; + + if (content.data && content.type && !isCipher(content.data)) { + note.content = { data: content.data, type: content.type - } - }); + }; + } + + if (content.title) { + note.title = content.title; + } + + await this.db.notes.add(note); } } diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index 44ff11976..fe4127d36 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -6552,6 +6552,10 @@ msgstr "This may take a while" msgid "This must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co." msgstr "This must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co." +#: src/strings.ts:2623 +msgid "This note is empty" +msgstr "This note is empty" + #: src/strings.ts:1594 msgid "This note is locked" msgstr "This note is locked" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 1227b767d..7a5ae104d 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -6511,6 +6511,10 @@ msgstr "" msgid "This must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co." msgstr "" +#: src/strings.ts:2623 +msgid "This note is empty" +msgstr "" + #: src/strings.ts:1594 msgid "This note is locked" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index eae277179..d05967901 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2631,5 +2631,6 @@ Use this if changes from other devices are not appearing on this device. This wi unsetExpiry: () => t`Unset expiry`, expiryDate: () => t`Expiry date`, exportCsv: () => t`Export CSV`, - importCsv: () => t`Import CSV` + importCsv: () => t`Import CSV`, + noContent: () => t`This note is empty` }; From fea581895192db1f921bba0819469152115b6a5e Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 12 Jan 2026 11:56:20 +0500 Subject: [PATCH 04/10] core: update note history tests --- packages/core/__tests__/note-history.test.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/__tests__/note-history.test.js b/packages/core/__tests__/note-history.test.js index 2e8e805e4..c1dcd7561 100644 --- a/packages/core/__tests__/note-history.test.js +++ b/packages/core/__tests__/note-history.test.js @@ -222,7 +222,7 @@ test("note history item can be created by setting note title and content both", expect(await db.noteHistory.get(id).count()).toBe(1); const history = db.noteHistory.get(id); const items = await history.items(); - const content = await db.noteHistory.sessionContent.get( + let content = await db.noteHistory.sessionContent.get( items[0].sessionContentId ); expect(await db.noteHistory.collection.count()).toBe(1); @@ -233,6 +233,12 @@ test("note history item can be created by setting note title and content both", content: TEST_NOTE.content, sessionId: "notesession" }); + + expect(await db.noteHistory.collection.count()).toBe(1); + content = await db.noteHistory.sessionContent.get( + items[0].sessionContentId + ); + expect(content.data).toBe(TEST_NOTE.content.data); expect(content.title).toBe("Test note"); await db.notes.add({ @@ -240,6 +246,13 @@ test("note history item can be created by setting note title and content both", title: "Test note", sessionId: "notesession" }); + + expect(await db.noteHistory.collection.count()).toBe(1); + content = await db.noteHistory.sessionContent.get( + items[0].sessionContentId + ); + console.log(content); + expect(content.data).toBe(TEST_NOTE.content.data); expect(content.title).toBe("Test note"); })); From 6b2bf369ff8cccc95f5e73219240a6cba80b089e Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 12 Jan 2026 12:36:17 +0500 Subject: [PATCH 05/10] core: ensure session content remains consistent when new session is created New sessions can be created with title or content only, hence we need to pull the missing data from the note itself to keep session history consistent with previous sessions --- packages/core/__tests__/note-history.test.js | 46 ++++++++++++++++++- packages/core/src/collections/notes.ts | 2 +- .../core/src/collections/session-content.ts | 24 ++++++++-- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/packages/core/__tests__/note-history.test.js b/packages/core/__tests__/note-history.test.js index c1dcd7561..7fc57c73d 100644 --- a/packages/core/__tests__/note-history.test.js +++ b/packages/core/__tests__/note-history.test.js @@ -251,7 +251,6 @@ test("note history item can be created by setting note title and content both", content = await db.noteHistory.sessionContent.get( items[0].sessionContentId ); - console.log(content); expect(content.data).toBe(TEST_NOTE.content.data); expect(content.title).toBe("Test note"); @@ -314,3 +313,48 @@ test("restoring an old session should replace note's content and title", () => ); } )); + +test.only("note history item has consistent note and title data between sessions", () => + noteTest({ + ...TEST_NOTE, + title: "Test note", + sessionId: "notesession1" + }).then(async ({ db, id }) => { + expect(await db.noteHistory.get(id).count()).toBe(1); + const history = db.noteHistory.get(id); + let items = await history.items(); + let content = await db.noteHistory.sessionContent.get( + items[0].sessionContentId + ); + expect(await db.noteHistory.collection.count()).toBe(1); + expect(content.data).toBe(TEST_NOTE.content.data); + expect(content.title).toBe("Test note"); + + await db.notes.add({ + id: id, + content: TEST_NOTE.content, + sessionId: "notesession2" + }); + + items = await history.items(); + content = await db.noteHistory.sessionContent.get( + items[1].sessionContentId + ); + + expect(content.data).toBe(TEST_NOTE.content.data); + expect(content.title).toBe("Test note"); + + await db.notes.add({ + id: id, + title: "Test note updated", + sessionId: "notesession3" + }); + + items = await history.items(); + content = await db.noteHistory.sessionContent.get( + items[2].sessionContentId + ); + + expect(content.data).toBe(TEST_NOTE.content.data); + expect(content.title).toBe("Test note updated"); + })); diff --git a/packages/core/src/collections/notes.ts b/packages/core/src/collections/notes.ts index 7d3364f4d..ad5c868e5 100644 --- a/packages/core/src/collections/notes.ts +++ b/packages/core/src/collections/notes.ts @@ -206,7 +206,7 @@ export class Notes implements ICollection { this.totalNotes++; } - if (item.sessionId) { + if (item.sessionId && typeof item.title === "string") { await this.db.noteHistory.add(item.sessionId, { title: item.title, noteId: id diff --git a/packages/core/src/collections/session-content.ts b/packages/core/src/collections/session-content.ts index 1931b0f7f..b5d112656 100644 --- a/packages/core/src/collections/session-content.ts +++ b/packages/core/src/collections/session-content.ts @@ -44,7 +44,7 @@ export class SessionContent implements ICollection { async add( sessionId: string, - content: Partial> & { title?: string }, + content: Partial> & { title?: string; noteId: string }, locked?: TLocked ) { if (!sessionId || !content) return; @@ -52,8 +52,10 @@ export class SessionContent implements ICollection { // locked || isCipher(content.data) // ? content.data // : await this.db.compressor().compress(content.data); - const sessionContentItemId = makeSessionContentId(sessionId); + const sessionContentExists = await this.collection.exists( + sessionContentItemId + ); const sessionItem: Partial = { type: "sessioncontent", id: sessionContentItemId, @@ -67,13 +69,29 @@ export class SessionContent implements ICollection { if (content.data && content.type) { sessionItem.data = content.data; sessionItem.contentType = content.type; + + if (typeof content.title !== "string" && !sessionContentExists) { + const note = await this.db.notes.note(content.noteId); + sessionItem.title = note?.title; + } } if (content.title) { sessionItem.title = content.title; + + if (!content.data && !content.type && !sessionContentExists) { + const note = await this.db.notes.note(content.noteId); + if (note?.contentId) { + const noteContent = await this.db.content.get(note?.contentId); + if (noteContent) { + sessionItem.data = noteContent?.data; + sessionItem.contentType = noteContent?.type; + } + } + } } - if (await this.collection.exists(sessionContentItemId)) { + if (sessionContentExists) { this.collection.update([sessionContentItemId], sessionItem); } else { await this.collection.upsert(sessionItem as SessionContentItem); From 2aa3aaf2f8684e22b4cb2a90c13d6c1e2d58842f Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 12 Jan 2026 12:52:24 +0500 Subject: [PATCH 06/10] mobile: cleanup --- apps/mobile/app/screens/editor/tiptap/use-editor.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor.ts b/apps/mobile/app/screens/editor/tiptap/use-editor.ts index a2123fbfa..aa351882a 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-editor.ts @@ -299,7 +299,6 @@ export const useEditor = ( } if (!title && isContentInvalid(data) && id) { - console.log("new editor session", title, isContentInvalid(data), id); // Create a new history session if recieved empty or invalid content // To ensure that history is preserved for correct content. currentSessionHistoryId = editorSessionHistory.newSession(id); @@ -313,8 +312,6 @@ export const useEditor = ( sessionId: `${currentSessionHistoryId}` }; - console.log(noteData.sessionId); - noteData.title = title; if (ignoreEdit) { From a91983340cac28f521655d9401c28716ee028a8c Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:55:39 +0500 Subject: [PATCH 07/10] web: support note title from session history in diff viewer Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- apps/web/src/components/diff-viewer/index.tsx | 13 +++++++++---- apps/web/src/stores/editor-store.ts | 6 ++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/diff-viewer/index.tsx b/apps/web/src/components/diff-viewer/index.tsx index 013e738e0..c411194d4 100644 --- a/apps/web/src/components/diff-viewer/index.tsx +++ b/apps/web/src/components/diff-viewer/index.tsx @@ -105,11 +105,16 @@ function DiffViewer(props: DiffViewerProps) { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", - textAlign: "center" + textAlign: "center", + paddingTop: 10 }} - > - {session.note.title} - + dangerouslySetInnerHTML={{ + __html: + session.type === "diff" + ? diff(session.oldContentTitle || "", session.note.title) + : session.note.title + }} + > {session.type === "diff" ? ( <> diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts index d10a53da8..a649285bc 100644 --- a/apps/web/src/stores/editor-store.ts +++ b/apps/web/src/stores/editor-store.ts @@ -145,6 +145,7 @@ export type DiffEditorSession = BaseEditorSession & { note: Note; content: ContentItem; historySessionId: string; + oldContentTitle?: string; }; export type EditorSession = @@ -658,9 +659,10 @@ class EditorStore extends BaseStore { note, tabId, title: label, + oldContentTitle: oldContent.title, historySessionId: session.id, content: { - type: oldContent.type, + type: oldContent.type || "tiptap", dateCreated: session.dateCreated, dateEdited: session.dateModified, dateModified: session.dateModified, @@ -670,7 +672,7 @@ class EditorStore extends BaseStore { conflicted: currentContent, ...(isCipher(oldContent.data) ? { locked: true, data: oldContent.data } - : { locked: false, data: oldContent.data }) + : { locked: false, data: oldContent.data || "" }) } }); }; From 2b0e5c41c8d3093fbd383fbe097905771721b704 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Fri, 16 Jan 2026 12:40:42 +0500 Subject: [PATCH 08/10] core: remove note content if history item has no content --- packages/core/src/collections/note-history.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/core/src/collections/note-history.ts b/packages/core/src/collections/note-history.ts index 0ebff07c4..18ca7ebb6 100644 --- a/packages/core/src/collections/note-history.ts +++ b/packages/core/src/collections/note-history.ts @@ -206,10 +206,7 @@ export class NoteHistory implements ICollection { title: content.title }); } - } else if ( - (content.data && content.type && !isCipher(content.data)) || - content.title - ) { + } else { const note: Partial< Note & { content: NoteContent; @@ -220,12 +217,13 @@ export class NoteHistory implements ICollection { sessionId: `${Date.now()}` }; - if (content.data && content.type && !isCipher(content.data)) { - note.content = { - data: content.data, - type: content.type - }; - } + note.content = + content.data && content.type && !isCipher(content.data) + ? { + data: content.data, + type: content.type + } + : { data: "

", type: "tiptap" }; if (content.title) { note.title = content.title; From 29f789493e547ef090ed9a7040f1ac49c7221098 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Fri, 16 Jan 2026 12:41:12 +0500 Subject: [PATCH 09/10] web: fix previewing note history where only title has changed --- apps/web/src/components/diff-viewer/index.tsx | 370 +++++++++--------- apps/web/src/stores/editor-store.ts | 15 +- 2 files changed, 202 insertions(+), 183 deletions(-) diff --git a/apps/web/src/components/diff-viewer/index.tsx b/apps/web/src/components/diff-viewer/index.tsx index c411194d4..724e09722 100644 --- a/apps/web/src/components/diff-viewer/index.tsx +++ b/apps/web/src/components/diff-viewer/index.tsx @@ -17,13 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { - useCallback, - useEffect, - useLayoutEffect, - useRef, - useState -} from "react"; +import { useCallback, useEffect, useLayoutEffect, useState } from "react"; import { Flex, Text, Button } from "@theme-ui/components"; import { Copy, Restore } from "../icons"; import ContentToggle from "./content-toggle"; @@ -59,6 +53,8 @@ function DiffViewer(props: DiffViewerProps) { const onResolveContent = useCallback( (saveCopy: boolean) => { + if (!conflictedContent) return; + const toKeep = selectedContent === 1 ? session.content?.conflicted : session.content; const toCopy = @@ -69,7 +65,7 @@ function DiffViewer(props: DiffViewerProps) { toKeep, toCopy: saveCopy ? toCopy : undefined, toKeepDateEdited: toKeep.dateEdited, - dateResolved: conflictedContent!.dateModified + dateResolved: conflictedContent.dateModified }); }, [conflictedContent, selectedContent, session.content, session.note] @@ -82,8 +78,6 @@ function DiffViewer(props: DiffViewerProps) { }); }, [session]); - if (!conflictedContent || !content) return null; - return ( { @@ -110,8 +104,8 @@ function DiffViewer(props: DiffViewerProps) { }} dangerouslySetInnerHTML={{ __html: - session.type === "diff" - ? diff(session.oldContentTitle || "", session.note.title) + session.type === "diff" && session.oldTitle + ? diff(session.oldTitle || "", session.note.title) : session.note.title }} > @@ -145,28 +139,34 @@ function DiffViewer(props: DiffViewerProps) { {strings.restoreThisVersion()} - + await notesStore.refresh(); + await openSession(noteId); + }} + mr={2} + sx={{ + alignItems: "center", + justifyContent: "center", + display: "flex" + }} + > + + {strings.saveACopy()} + + )}