Merge branch 'master' into fix-localization-error

Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
This commit is contained in:
Ammar Ahmed
2024-12-21 14:09:22 +05:00
committed by GitHub
27 changed files with 1332 additions and 1084 deletions

View File

@@ -10,7 +10,7 @@ const authors = readFileSync("AUTHORS", "utf-8");
const isAuthor = authors.includes(`<${authorEmail}>`); const isAuthor = authors.includes(`<${authorEmail}>`);
const SCOPES = [ const SCOPES = [
// for full list of scopes + details see: https://github.com/streetwriters/notesnook-private/blob/master/CONTRIBUTING.md#commit-guidelines // for full list of scopes + details see: https://github.com/streetwriters/notesnook/blob/master/CONTRIBUTING.md#commit-guidelines
"mobile", "mobile",
"web", "web",
@@ -36,7 +36,8 @@ const SCOPES = [
"global", "global",
"docs", "docs",
"themebuilder", "themebuilder",
"intl" "intl",
"webclipper"
]; ];
module.exports = { module.exports = {

View File

@@ -231,6 +231,23 @@ test("add tags to note", async ({ page }) => {
expect(noteTags.every((t, i) => t === tags[i])).toBe(true); expect(noteTags.every((t, i) => t === tags[i])).toBe(true);
}); });
test("add tags to locked note", async ({ page }) => {
const tags = ["incognito", "secret-stuff"];
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.lock(PASSWORD);
await note?.openLockedNote(PASSWORD);
await notes.editor.setTags(tags);
await page.waitForTimeout(200);
const noteTags = await notes.editor.getTags();
expect(noteTags).toHaveLength(tags.length);
expect(noteTags.every((t, i) => t === tags[i])).toBe(true);
});
for (const format of ["html", "txt", "md"] as const) { for (const format of ["html", "txt", "md"] as const) {
test(`export note as ${format}`, async ({ page }) => { test(`export note as ${format}`, async ({ page }) => {
const app = new AppModel(page); const app = new AppModel(page);

View File

@@ -66,7 +66,7 @@ class Vault {
subtitle: strings.deleteVaultDesc(), subtitle: strings.deleteVaultDesc(),
inputs: { inputs: {
password: { password: {
label: strings.password(), label: strings.accountPassword(),
autoComplete: "current-password" autoComplete: "current-password"
} }
}, },

View File

@@ -834,6 +834,7 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
if (!note || !note.content) if (!note || !note.content)
throw new Error("note with this id does not exist."); throw new Error("note with this id does not exist.");
const tags = await db.notes.tags(note.id);
useEditorStore.getState().addSession({ useEditorStore.getState().addSession({
type: session.note.readonly ? "readonly" : "default", type: session.note.readonly ? "readonly" : "default",
locked: true, locked: true,
@@ -841,6 +842,7 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
note: session.note, note: session.note,
saveState: SaveState.Saved, saveState: SaveState.Saved,
sessionId: `${Date.now()}`, sessionId: `${Date.now()}`,
tags,
pinned: session.pinned, pinned: session.pinned,
preview: session.preview, preview: session.preview,
content: note.content content: note.content

View File

@@ -369,11 +369,7 @@ function TipTap(props: TipTapProps) {
<Toolbar <Toolbar
editor={editor} editor={editor}
location={"top"} location={"top"}
sx={ sx={isTablet || isMobile ? { flexWrap: "nowrap" } : {}}
isTablet || isMobile
? { overflowX: "scroll", flexWrap: "nowrap" }
: {}
}
tools={toolbarConfig} tools={toolbarConfig}
defaultFontFamily={fontFamily} defaultFontFamily={fontFamily}
defaultFontSize={fontSize} defaultFontSize={fontSize}
@@ -460,7 +456,7 @@ function TiptapWrapper(
: EDITOR_ZOOM.STEP; : EDITOR_ZOOM.STEP;
const zoom = Math.min( const zoom = Math.min(
EDITOR_ZOOM.MAX, EDITOR_ZOOM.MAX,
Math.max(EDITOR_ZOOM.MIN, editorConfig.zoom + delta) Math.max(EDITOR_ZOOM.MIN, Math.round(editorConfig.zoom + delta))
); );
setEditorConfig({ zoom }); setEditorConfig({ zoom });
} }

View File

@@ -127,12 +127,19 @@ export const IssueDialog = DialogManager.register(function IssueDialog(
<Link <Link
href="https://github.com/streetwriters/notesnook/issues" href="https://github.com/streetwriters/notesnook/issues"
title="github.com/streetwriters/notesnook/issues" title="github.com/streetwriters/notesnook/issues"
/>{" "} target="_blank"
>
github.com/streetwriters/notesnook/issues
</Link>
{strings.issueNotice[1]()}{" "} {strings.issueNotice[1]()}{" "}
<Link <Link
href="https://discord.gg/zQBK97EE22" href="https://discord.gg/zQBK97EE22"
title={strings.issueNotice[2]()} title={strings.issueNotice[2]()}
/> target="_blank"
>
{strings.issueNotice[2]()}
</Link>
/
</Text> </Text>
<Text variant="subBody" mt={1}> <Text variant="subBody" mt={1}>
{getDeviceInfo([`Pro: ${isUserPremium()}`]) {getDeviceInfo([`Pro: ${isUserPremium()}`])

View File

@@ -258,7 +258,7 @@ function ChooseAuthenticator(props: ChooseAuthenticatorProps) {
justifyContent: "start", justifyContent: "start",
alignItems: "start", alignItems: "start",
textAlign: "left", textAlign: "left",
bg: "transparent", bg: selected === index ? "shade" : "transparent",
px: 0 px: 0
}} }}
onClick={() => setSelected(index)} onClick={() => setSelected(index)}

View File

@@ -166,7 +166,11 @@ export function Importer() {
<Box as="ol" sx={{ my: 1 }}> <Box as="ol" sx={{ my: 1 }}>
<Text as="li" variant="body"> <Text as="li" variant="body">
Go to{" "} Go to{" "}
<Link href="https://importer.notesnook.com/" target="_blank"> <Link
href="https://importer.notesnook.com/"
target="_blank"
sx={{ color: "accent" }}
>
https://importer.notesnook.com/ https://importer.notesnook.com/
</Link> </Link>
</Text> </Text>

View File

@@ -421,7 +421,7 @@ class EditorStore extends BaseStore<EditorStore> {
continue; continue;
updateSession(session.id, undefined, { updateSession(session.id, undefined, {
tags: await getTags(session.note.id) tags: await db.notes.tags(session.note.id)
}); });
} }
} else if ( } else if (
@@ -432,7 +432,7 @@ class EditorStore extends BaseStore<EditorStore> {
event.item.toType === "note" event.item.toType === "note"
) { ) {
updateSession(event.item.toId, undefined, { updateSession(event.item.toId, undefined, {
tags: await getTags(event.item.toId) tags: await db.notes.tags(event.item.toId)
}); });
} }
} else if (event.collection === "tags") { } else if (event.collection === "tags") {
@@ -445,7 +445,7 @@ class EditorStore extends BaseStore<EditorStore> {
continue; continue;
console.log("UDPATE"); console.log("UDPATE");
updateSession(session.id, undefined, { updateSession(session.id, undefined, {
tags: await getTags(session.note.id) tags: await db.notes.tags(session.note.id)
}); });
} }
} }
@@ -671,7 +671,7 @@ class EditorStore extends BaseStore<EditorStore> {
const attachmentsLength = await db.attachments const attachmentsLength = await db.attachments
.ofNote(note.id, "all") .ofNote(note.id, "all")
.count(); .count();
const tags = await getTags(note.id); const tags = await db.notes.tags(note.id);
const colors = await db.relations.to(note, "color").get(); const colors = await db.relations.to(note, "color").get();
if (note.readonly) { if (note.readonly) {
this.addSession( this.addSession(
@@ -871,12 +871,19 @@ class EditorStore extends BaseStore<EditorStore> {
}; };
newSession = () => { newSession = () => {
this.addSession({ const state = useEditorStore.getState();
type: "new", const session = state.sessions.find((session) => session.type === "new");
id: getId(), if (session) {
context: useNoteStore.getState().context, session.context = useNoteStore.getState().context;
saveState: SaveState.NotSaved this.activateSession(session.id);
}); } else {
this.addSession({
type: "new",
id: getId(),
context: useNoteStore.getState().context,
saveState: SaveState.NotSaved
});
}
}; };
closeSessions = (...ids: string[]) => { closeSessions = (...ids: string[]) => {
@@ -1020,12 +1027,3 @@ async function waitForSync() {
db.eventManager.subscribe(EVENTS.syncCompleted, resolve, true); db.eventManager.subscribe(EVENTS.syncCompleted, resolve, true);
}); });
} }
async function getTags(noteId: string) {
return await db.relations
.to({ id: noteId, type: "note" }, "tag")
.selector.items(undefined, {
sortBy: "dateCreated",
sortDirection: "asc"
});
}

View File

@@ -19,6 +19,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import snarkdown from "snarkdown"; import snarkdown from "snarkdown";
export function mdToHtml(markdown: string) { function addAttributes(
return snarkdown(markdown); html: string,
tag: keyof HTMLElementTagNameMap,
attributes: Record<string, string>
) {
const temp = document.createElement("div");
temp.innerHTML = html;
const elements = temp.querySelectorAll(tag);
elements.forEach((element) => {
Object.entries(attributes).forEach(([key, value]) => {
element.setAttribute(key, value);
});
});
return temp.innerHTML;
}
export function mdToHtml(markdown: string) {
return addAttributes(snarkdown(markdown), "a", { target: "_blank" });
} }

View File

@@ -985,7 +985,14 @@ export function AuthField(props: FieldProps) {
p: "12px", p: "12px",
borderRadius: "default", borderRadius: "default",
bg: "background", bg: "background",
boxShadow: "0px 0px 5px 0px #00000019" boxShadow: "0px 0px 5px 0px #00000019",
"::-moz-appearance": "textfield",
"::-webkit-inner-spin-button": {
"-webkit-appearance": "none"
},
"::-webkit-outer-spin-button": {
"-webkit-appearance": "none"
}
} }
}} }}
/> />

View File

@@ -0,0 +1,30 @@
# Contributing guidelines
Please read the [contributing guidelines](../../CONTRIBUTING.md) beforehand.
### Setting web clipper locally
#### Running the web clipper
1. Install packages and setup the repo. Run this command in the repository root:
```sh
npm install
```
1. Run the Notesnook webapp:
```sh
npm run start:web
```
1. Navigate to the web clipper folder:
```sh
cd extensions/web-clipper
```
1. Run the web clipper:
```sh
npm run dev:chrome
```
#### Viewing the web clipper
1. Open chrome and go to `chrome://extensions`.
1. Turn on "Developer Mode".
1. Click on "Load unpacked" and select the `extensions/web-clipper/build` folder.

View File

@@ -75,6 +75,8 @@ function attachMessagePort() {
height: document.body.clientHeight, height: document.body.clientHeight,
width: document.body.clientWidth width: document.body.clientWidth
}; };
default:
return false;
} }
}); });
} }

View File

@@ -197,6 +197,15 @@ test("update note", () =>
expect(note?.favorite).toBe(true); expect(note?.favorite).toBe(true);
})); }));
test("get note tags", () =>
noteTest({
...TEST_NOTE
}).then(async ({ db, id }) => {
const tag = await db.tags.add({ title: "hello" });
await db.relations.add({ type: "tag", id: tag }, { type: "note", id });
expect(await db.notes.tags(id)).toEqual([await db.tags.tag(tag)]);
}));
test("get favorite notes", () => test("get favorite notes", () =>
noteTest({ noteTest({
...TEST_NOTE, ...TEST_NOTE,

View File

@@ -176,6 +176,15 @@ export class Notes implements ICollection {
return note; return note;
} }
async tags(id: string) {
return this.db.relations
.to({ id, type: "note" }, "tag")
.selector.items(undefined, {
sortBy: "dateCreated",
sortDirection: "asc"
});
}
// note(idOrNote: string | Note) { // note(idOrNote: string | Note) {
// if (!idOrNote) return; // if (!idOrNote) return;
// const note = // const note =

View File

@@ -0,0 +1,50 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import { describe, expect, it } from "vitest";
import { parseInternalLink } from "../internal-link";
describe("parseInternalLink", () => {
const invalidInternalLinks = [
"",
"invalid-url",
"http://google.com",
"https://google.com"
];
invalidInternalLinks.forEach((url) => {
it(`should return undefined when not internal link: ${url}`, () => {
expect(parseInternalLink(url)).toBeUndefined();
});
});
const validInternalLinks = [
{
url: "nn://note/123",
expected: { type: "note", id: "123", params: {} }
},
{
url: "nn://note/123?blockId=456",
expected: { type: "note", id: "123", params: { blockId: "456" } }
}
];
validInternalLinks.forEach(({ url, expected }) => {
it(`should parse internal link: ${url}`, () => {
expect(parseInternalLink(url)).toEqual(expected);
});
});
});

View File

@@ -52,7 +52,12 @@ export function createInternalLink<T extends InternalLinkType>(
} }
export function parseInternalLink(link: string): InternalLink | undefined { export function parseInternalLink(link: string): InternalLink | undefined {
const url = new URL(link); let url;
try {
url = new URL(link);
} catch (e) {
return;
}
if (url.protocol !== "nn:") return; if (url.protocol !== "nn:") return;
const [type, id] = url.href.split("?")[0].split("/").slice(2); const [type, id] = url.href.split("?")[0].split("/").slice(2);

View File

@@ -19,8 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
const MACHINE_ID = Math.floor(Math.random() * 0xffffff); const MACHINE_ID = Math.floor(Math.random() * 0xffffff);
const pid = Math.floor(Math.random() * 100000) % 0xffff; const pid = Math.floor(Math.random() * 100000) % 0xffff;
const PROCESS_UNIQUE = MACHINE_ID.toString(16).padStart(6, "0") + pid.toString(16).padStart(4, "0");
let index = Math.floor(Math.random() * 0xffffff); let index = Math.floor(Math.random() * 0xffffff);
const PROCESS_UNIQUE = MACHINE_ID.toString(16) + pid.toString(16);
export function createObjectId(date = Date.now()): string { export function createObjectId(date = Date.now()): string {
index++; index++;
const time = Math.floor(date / 1000); const time = Math.floor(date / 1000);
@@ -40,4 +41,4 @@ function swap16(val: number) {
export function getObjectIdTimestamp(id: string) { export function getObjectIdTimestamp(id: string) {
return new Date(parseInt(id.substring(0, 8), 16) * 1000); return new Date(parseInt(id.substring(0, 8), 16) * 1000);
} }

View File

@@ -0,0 +1,21 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import { InlineCode } from "./inline-code";
export default InlineCode;

View File

@@ -0,0 +1,22 @@
import Code from "@tiptap/extension-code";
export const InlineCode = Code.extend({
excludes: "link",
addAttributes() {
return {
...this.parent?.(),
spellcheck: {
default: "false",
parseHTML: (element) => element.getAttribute("spellcheck"),
renderHTML: (attributes) => {
if (!attributes.spellcheck) {
return {};
}
return {
spellcheck: attributes.spellcheck
};
}
}
};
}
});

View File

@@ -0,0 +1,37 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import { expect, test } from "vitest";
import { createEditor, h } from "../../../../test-utils/index.js";
import { Link } from "../../link/link.js";
import { InlineCode } from "../inline-code";
test("inline code has spellcheck disabled", async () => {
const el = h("code", ["blazingly fast javascript"]);
const editor = createEditor({
initialContent: el.outerHTML,
extensions: {
link: Link,
code: InlineCode
}
});
expect(
editor.editor.view.dom.querySelector("code")?.getAttribute("spellcheck")
).toBe("false");
});

View File

@@ -23,7 +23,6 @@ import {
getHTMLFromFragment getHTMLFromFragment
} from "@tiptap/core"; } from "@tiptap/core";
import CharacterCount from "@tiptap/extension-character-count"; import CharacterCount from "@tiptap/extension-character-count";
import { Code } from "@tiptap/extension-code";
import Color from "@tiptap/extension-color"; import Color from "@tiptap/extension-color";
import HorizontalRule from "@tiptap/extension-horizontal-rule"; import HorizontalRule from "@tiptap/extension-horizontal-rule";
import { Link, LinkAttributes } from "./extensions/link/index.js"; import { Link, LinkAttributes } from "./extensions/link/index.js";
@@ -85,6 +84,7 @@ import { useEditorSearchStore } from "./toolbar/stores/search-store.js";
import { DiffHighlighter } from "./extensions/diff-highlighter/index.js"; import { DiffHighlighter } from "./extensions/diff-highlighter/index.js";
import { getChangedNodes } from "./utils/prosemirror.js"; import { getChangedNodes } from "./utils/prosemirror.js";
import { strings } from "@notesnook/intl"; import { strings } from "@notesnook/intl";
import { InlineCode } from "./extensions/inline-code/inline-code.js";
interface TiptapStorage { interface TiptapStorage {
dateFormat?: DateTimeOptions["dateFormat"]; dateFormat?: DateTimeOptions["dateFormat"];
@@ -302,7 +302,7 @@ const useTiptap = (
OutlineListItem, OutlineListItem,
OutlineList.configure({ keepAttributes: true, keepMarks: true }), OutlineList.configure({ keepAttributes: true, keepMarks: true }),
ListItem, ListItem,
Code.extend({ excludes: "link" }), InlineCode,
Codemark, Codemark,
MathInline, MathInline,
MathBlock, MathBlock,

View File

@@ -34,6 +34,7 @@ import {
useToolbarStore useToolbarStore
} from "./stores/toolbar-store.js"; } from "./stores/toolbar-store.js";
import { ToolbarDefinition } from "./types.js"; import { ToolbarDefinition } from "./types.js";
import { ScrollContainer } from "@notesnook/ui";
type ToolbarProps = FlexProps & { type ToolbarProps = FlexProps & {
editor: Editor; editor: Editor;
@@ -89,34 +90,44 @@ export function Toolbar(props: ToolbarProps) {
return ( return (
<> <>
<Flex <ScrollContainer
className={["editor-toolbar", className].join(" ")} className="tabsScroll"
sx={{ suppressScrollY
flexWrap: isMobile ? "nowrap" : "wrap", style={{ flex: 1 }}
overflowX: isMobile ? "auto" : "hidden", trackStyle={() => ({
bg: "background", backgroundColor: "transparent",
borderRadius: isMobile ? "0px" : "default", pointerEvents: "none"
...sx
}}
{...flexProps}
>
{toolbarTools.map((tools) => {
return (
<ToolbarGroup
key={tools.join("")}
tools={tools}
editor={editor}
groupId={tools.join("")}
sx={{
borderRight: "1px solid var(--separator)",
":last-of-type": { borderRight: "none" },
alignItems: "center"
}}
/>
);
})} })}
</Flex> thumbStyle={() => ({ height: 3 })}
<EditorFloatingMenus editor={editor} /> >
<Flex
className={["editor-toolbar", className].join(" ")}
sx={{
flexWrap: isMobile ? "nowrap" : "wrap",
bg: "background",
borderRadius: isMobile ? "0px" : "default",
...sx
}}
{...flexProps}
>
{toolbarTools.map((tools) => {
return (
<ToolbarGroup
key={tools.join("")}
tools={tools}
editor={editor}
groupId={tools.join("")}
sx={{
borderRight: "1px solid var(--separator)",
":last-of-type": { borderRight: "none" },
alignItems: "center"
}}
/>
);
})}
</Flex>
<EditorFloatingMenus editor={editor} />
</ScrollContainer>
</> </>
); );
} }

View File

@@ -107,6 +107,7 @@ export function EditLink(props: ToolProps) {
const link = node ? findMark(node, LinkNode.name) : null; const link = node ? findMark(node, LinkNode.name) : null;
const attrs = link?.attrs || getMarkAttributes(editor.state, LinkNode.name); const attrs = link?.attrs || getMarkAttributes(editor.state, LinkNode.name);
if (!editor.isEditable) return null;
if (attrs && isInternalLink(attrs.href)) if (attrs && isInternalLink(attrs.href))
return ( return (
<ToolButton <ToolButton

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1692,18 +1692,20 @@ For example:
t`Your account is now 100% secure against unauthorized logins.`, t`Your account is now 100% secure against unauthorized logins.`,
sms: () => t`phone number`, sms: () => t`phone number`,
app: () => t`authentication app`, app: () => t`authentication app`,
mfaFallbackMethodText: (fallback: string, primary: string) => mfaFallbackMethodText: (
`You will now receive your 2FA codes on your ${ fallback: "app" | "sms" | "email",
strings[fallback as keyof typeof strings] primary: "app" | "sms" | "email"
} in case you lose access to your ${ ) =>
strings[primary as keyof typeof strings] `You will now receive your 2FA codes on your ${strings[
}.`, fallback
transactionStatusToText: ( ]().toLocaleLowerCase()} in case you lose access to your ${strings[
key: keyof typeof TRANSACTION_STATUS | ({} & string) primary
) => { ]().toLocaleLowerCase()}.`,
return key in TRANSACTION_STATUS transactionStatusToText: {
? TRANSACTION_STATUS[key as keyof typeof TRANSACTION_STATUS]() completed: () => t`Completed`,
: key; refunded: () => t`"Refunded`,
partially_refunded: () => t`Partially refunded`,
disputed: () => t`Disputed`
}, },
viewReceipt: () => t`View receipt`, viewReceipt: () => t`View receipt`,
customDictWords: (count: number) => customDictWords: (count: number) =>