Merge pull request #9192 from streetwriters/core/note-history-title

Add support for saving title in note history sessions
This commit is contained in:
Abdullah Atta
2026-01-16 12:41:49 +05:00
committed by GitHub
14 changed files with 550 additions and 235 deletions

View File

@@ -47,7 +47,7 @@ const HistoryItem = ({
}: {
index: number;
items?: VirtualizedGrouping<HistorySession>;
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 }) => (
<HistoryItem index={index} items={history} />
<HistoryItem index={index} items={history} note={note} />
),
[history]
);

View File

@@ -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<boolean> & {
title: string;
}
>
| undefined;
note: TrashOrItem<Note>;
}) {
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 }) {
}}
>
<Dialog context="local" />
<DialogHeader padding={12} title={note?.title || session?.session} />
{!session?.locked && !locked ? (
<DialogHeader
padding={12}
title={content?.title || note.title || session?.session}
/>
{!session?.locked && !locked && content?.data ? (
<View
style={{
flex: 1,
@@ -125,17 +152,27 @@ export default function NotePreview({ session, content, note }) {
editorId="historyPreview"
onLoad={async (loadContent) => {
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 || "<p></p>",
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 }) {
}}
>
<Paragraph color={colors.secondary.paragraph}>
{strings.encryptedNoteHistoryNotice()}
{!content?.data
? strings.noContent()
: strings.encryptedNoteHistoryNotice()}
</Paragraph>
</View>
)}

View File

@@ -298,7 +298,7 @@ export const useEditor = (
return;
}
if (isContentInvalid(data) && id) {
if (!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);

View File

@@ -17,13 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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 (
<Flex
ref={(el) => {
@@ -105,11 +99,16 @@ function DiffViewer(props: DiffViewerProps) {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
textAlign: "center"
textAlign: "center",
paddingTop: 10
}}
>
{session.note.title}
</Text>
dangerouslySetInnerHTML={{
__html:
session.type === "diff" && session.oldTitle
? diff(session.oldTitle || "", session.note.title)
: session.note.title
}}
></Text>
<Flex mt={1} sx={{ alignSelf: "center", justifySelf: "center" }}>
{session.type === "diff" ? (
<>
@@ -140,28 +139,34 @@ function DiffViewer(props: DiffViewerProps) {
<Restore size={18} />
<Text ml={1}>{strings.restoreThisVersion()}</Text>
</Button>
<Button
variant="secondary"
onClick={async () => {
const { closeTabs, openSession } = useEditorStore.getState();
{!content || !conflictedContent ? null : (
<Button
variant="secondary"
onClick={async () => {
const { closeTabs, openSession } = useEditorStore.getState();
const noteId = await createCopy(session.note, content);
const noteId = await createCopy(
session.note,
content,
session.oldTitle
);
closeTabs(session.id);
closeTabs(session.id);
await notesStore.refresh();
await openSession(noteId);
}}
mr={2}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
<Copy size={18} />
<Text ml={1}>{strings.saveACopy()}</Text>
</Button>
await notesStore.refresh();
await openSession(noteId);
}}
mr={2}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
<Copy size={18} />
<Text ml={1}>{strings.saveACopy()}</Text>
</Button>
)}
<Button
variant="errorSecondary"
onClick={() => {
@@ -174,165 +179,183 @@ function DiffViewer(props: DiffViewerProps) {
</>
) : null}
</Flex>
<ScrollSync>
<Flex
{!conflictedContent || !content ? (
<Text
variant="body"
sx={{
flex: "1 1 auto",
flexDirection: ["column", "column", "row"],
overflow: "hidden"
px: 2,
py: 1,
my: 1,
borderTop: "1px solid var(--border)"
}}
>
{strings.noContent()}
</Text>
) : (
<ScrollSync>
<Flex
className="firstEditor"
data-test-id="first-editor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"]
flexDirection: ["column", "column", "row"],
overflow: "hidden"
}}
>
{content.locked ? (
<UnlockView
title={getFormattedDate(content.dateEdited)}
subtitle={strings.enterPasswordToUnlockVersion()}
buttonTitle={strings.unlock()}
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
content,
password
);
setContent({
...content,
...decryptedContent,
locked: false
});
}}
/>
) : (
<>
<ContentToggle
label={
session.type === "diff"
? strings.olderVersion()
: strings.currentNote()
}
readonly={session.type === "diff"}
dateEdited={content.dateEdited}
isSelected={selectedContent === 0}
isOtherSelected={selectedContent === 1}
onToggle={() => setSelectedContent((s) => (s === 0 ? -1 : 0))}
resolveConflict={onResolveContent}
sx={{
borderStyle: "solid",
borderWidth: 0,
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1
<Flex
className="firstEditor"
data-test-id="first-editor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"]
}}
>
{content.locked ? (
<UnlockView
title={getFormattedDate(content.dateEdited)}
subtitle={strings.enterPasswordToUnlockVersion()}
buttonTitle={strings.unlock()}
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
content,
password
);
setContent({
...content,
...decryptedContent,
locked: false
});
}}
/>
<ScrollSyncPane>
<Flex
) : (
<>
<ContentToggle
label={
session.type === "diff"
? strings.olderVersion()
: strings.currentNote()
}
readonly={session.type === "diff"}
dateEdited={content.dateEdited}
isSelected={selectedContent === 0}
isOtherSelected={selectedContent === 1}
onToggle={() =>
setSelectedContent((s) => (s === 0 ? -1 : 0))
}
resolveConflict={onResolveContent}
sx={{
px: 2,
overflowY: "auto",
flex: 1,
borderStyle: "solid",
borderWidth: 0,
borderRightWidth: [0, 0, 1],
borderBottomWidth: [1, 1, 0],
borderColor: "border"
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1
}}
>
<Editor
id={content.id}
content={() => content.data}
session={session}
nonce={content.dateEdited}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</>
)}
</Flex>
<Flex
className="secondEditor"
data-test-id="second-editor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"],
borderLeft: conflictedContent.locked
? "1px solid var(--border)"
: "none"
}}
>
{conflictedContent.locked ? (
<UnlockView
title={getFormattedDate(conflictedContent.dateEdited)}
subtitle={strings.enterPasswordToUnlockVersion()}
buttonTitle={strings.unlock()}
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
conflictedContent,
password
);
setConflictedContent({
...conflictedContent,
...decryptedContent,
locked: false
});
}}
/>
) : (
<>
<ContentToggle
readonly={session.type === "diff"}
resolveConflict={onResolveContent}
label={
session.type === "diff"
? strings.currentNote()
: strings.incomingNote()
}
isSelected={selectedContent === 1}
isOtherSelected={selectedContent === 0}
dateEdited={conflictedContent.dateEdited}
onToggle={() => setSelectedContent((s) => (s === 1 ? -1 : 1))}
sx={{
alignItems: "flex-end",
borderStyle: "solid",
borderWidth: 0,
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1,
pt: [1, 1, 0]
/>
<ScrollSyncPane>
<Flex
sx={{
px: 2,
overflowY: "auto",
flex: 1,
borderStyle: "solid",
borderWidth: 0,
borderRightWidth: [0, 0, 1],
borderBottomWidth: [1, 1, 0],
borderColor: "border"
}}
>
<Editor
id={content.id}
content={() => content.data}
session={session}
nonce={content.dateEdited}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</>
)}
</Flex>
<Flex
className="secondEditor"
data-test-id="second-editor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"],
borderLeft: conflictedContent?.locked
? "1px solid var(--border)"
: "none"
}}
>
{conflictedContent.locked ? (
<UnlockView
title={getFormattedDate(conflictedContent.dateEdited)}
subtitle={strings.enterPasswordToUnlockVersion()}
buttonTitle={strings.unlock()}
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
conflictedContent,
password
);
setConflictedContent({
...conflictedContent,
...decryptedContent,
locked: false
});
}}
/>
<ScrollSyncPane>
<Flex sx={{ px: 2, overflow: "auto" }}>
<Editor
id={`${conflictedContent.id}-conflicted`}
session={session}
content={() =>
content.locked
? conflictedContent.data
: diff(content.data, conflictedContent.data)
}
nonce={conflictedContent.dateEdited}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</>
)}
) : (
<>
<ContentToggle
readonly={session.type === "diff"}
resolveConflict={onResolveContent}
label={
session.type === "diff"
? strings.currentNote()
: strings.incomingNote()
}
isSelected={selectedContent === 1}
isOtherSelected={selectedContent === 0}
dateEdited={conflictedContent.dateEdited}
onToggle={() =>
setSelectedContent((s) => (s === 1 ? -1 : 1))
}
sx={{
alignItems: "flex-end",
borderStyle: "solid",
borderWidth: 0,
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1,
pt: [1, 1, 0]
}}
/>
<ScrollSyncPane>
<Flex sx={{ px: 2, overflow: "auto" }}>
<Editor
id={`${conflictedContent.id}-conflicted`}
session={session}
content={() =>
content.locked
? conflictedContent.data
: diff(content.data, conflictedContent.data)
}
nonce={conflictedContent.dateEdited}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</>
)}
</Flex>
</Flex>
</Flex>
</ScrollSync>
</ScrollSync>
)}
</Flex>
);
}
@@ -374,7 +397,7 @@ async function resolveConflict({
await notesStore.refresh();
}
async function createCopy(note: Note, content: ContentItem) {
async function createCopy(note: Note, content: ContentItem, title?: string) {
if (content.locked) {
const contentId = await db.content.add({
locked: true,
@@ -384,7 +407,7 @@ async function createCopy(note: Note, content: ContentItem) {
});
return await db.notes.add({
contentId,
title: note.title + " (COPY)"
title: title || note.title + " (COPY)"
});
} else {
return await db.notes.add({
@@ -392,7 +415,7 @@ async function createCopy(note: Note, content: ContentItem) {
type: "tiptap",
data: content.data
},
title: note.title + " (COPY)"
title: title || note.title + " (COPY)"
});
}
}

View File

@@ -144,6 +144,7 @@ export type DiffEditorSession = BaseEditorSession & {
type: "diff";
note: Note;
content: ContentItem;
oldTitle?: string;
historySessionId: string;
};
@@ -619,12 +620,13 @@ class EditorStore extends BaseStore<EditorStore> {
openDiffSession = async (noteId: string, sessionId: string) => {
const session = await db.noteHistory.session(sessionId);
const note = await db.notes.note(noteId);
if (!session || !note || !note.contentId) return;
if (!session || !note) return;
const currentContent = await db.content.get(note.contentId);
const currentContent = note.contentId
? await db.content.get(note.contentId)
: undefined;
const oldContent = await db.noteHistory.content(session.id);
if (!oldContent || !currentContent) return;
if (!oldContent) return;
const {
getSession,
@@ -658,9 +660,10 @@ class EditorStore extends BaseStore<EditorStore> {
note,
tabId,
title: label,
oldTitle: oldContent.title,
historySessionId: session.id,
content: {
type: oldContent.type,
type: oldContent.type || "tiptap",
dateCreated: session.dateCreated,
dateEdited: session.dateModified,
dateModified: session.dateModified,
@@ -670,7 +673,7 @@ class EditorStore extends BaseStore<EditorStore> {
conflicted: currentContent,
...(isCipher(oldContent.data)
? { locked: true, data: oldContent.data }
: { locked: false, data: oldContent.data })
: { locked: false, data: oldContent.data || "" })
}
});
};
@@ -1354,7 +1357,7 @@ const useEditorStore = createPersistedStore(EditorStore, {
}, [] as EditorSession[])
}),
storage: db.config() as PersistStorage<Partial<EditorStore>>
});
}) as ReturnType<typeof createPersistedStore<EditorStore>>;
export { useEditorStore, SESSION_STATES };
const MILLISECONDS_IN_A_MINUTE = 60 * 1000;

View File

@@ -199,3 +199,162 @@ 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({
...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();
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: "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({
id: id,
title: "Test note",
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");
}));
test("restoring an old session should replace note's content title", () =>
noteTest({ title: "Test note", sessionId: Date.now() }).then(
async ({ db, id }) => {
await delay(1000);
let newTitle = "Test note (edited)";
const sessionId = `${Date.now() + 10000}`;
await db.notes.add({
id: id,
title: newTitle,
sessionId
});
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 + "<p>Some new content</p>",
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
);
}
));
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");
}));

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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";
@@ -74,13 +74,19 @@ export class NoteHistory implements ICollection {
async add(
sessionId: string,
content: NoteContent<boolean> & { noteId: string; locked: boolean }
content: Partial<NoteContent<boolean>> & {
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;
@@ -182,22 +190,46 @@ 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 && !isCipher(content.data)) {
await this.db.notes.add({
id: session.noteId,
sessionId: `${Date.now()}`,
content: {
data: content.data,
type: content.type
if (content.title) {
await this.db.notes.add({
id: session.noteId,
sessionId: sessionId,
title: content.title
});
}
} else {
const note: Partial<
Note & {
content: NoteContent<false>;
sessionId: string;
}
});
> = {
id: session.noteId,
sessionId: `${Date.now()}`
};
note.content =
content.data && content.type && !isCipher(content.data)
? {
data: content.data,
type: content.type
}
: { data: "<p></p>", type: "tiptap" };
if (content.title) {
note.title = content.title;
}
await this.db.notes.add(note);
}
}

View File

@@ -205,6 +205,13 @@ export class Notes implements ICollection {
});
this.totalNotes++;
}
if (item.sessionId && typeof item.title === "string") {
await this.db.noteHistory.add(item.sessionId, {
title: item.title,
noteId: id
});
}
});
return id;
}

View File

@@ -44,30 +44,63 @@ export class SessionContent implements ICollection {
async add<TLocked extends boolean>(
sessionId: string,
content: NoteContent<TLocked>,
locked: TLocked
content: Partial<NoteContent<TLocked>> & { title?: string; noteId: 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 sessionContentExists = await this.collection.exists(
sessionContentItemId
);
const sessionItem: Partial<SessionContentItem> = {
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 (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 (sessionContentExists) {
this.collection.update([sessionContentItemId], sessionItem);
} else {
await this.collection.upsert(sessionItem as SessionContentItem);
}
}
async get(
sessionContentId: string
): Promise<NoteContent<boolean> | undefined> {
): Promise<Partial<NoteContent<boolean> & { title: string }> | undefined> {
const session = await this.collection.get(sessionContentId);
if (!session || isDeleted(session)) return;
@@ -90,7 +123,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
};
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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:2635
msgid "This note is empty"
msgstr "This note is empty"
#: src/strings.ts:1594
msgid "This note is locked"
msgstr "This note is locked"

View File

@@ -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:2635
msgid "This note is empty"
msgstr ""
#: src/strings.ts:1594
msgid "This note is locked"
msgstr ""

View File

@@ -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`
};