Compare commits

..

91 Commits

Author SHA1 Message Date
Ammar Ahmed
9c9eb4e175 mobile: fix ts errors 2025-02-11 10:38:12 +05:00
luis-411
0517c6ce41 web: fix status-bar not visible on medium sized screens (#7493)
Signed-off-by: Luis Kriner <luis@kriner.info>
2025-02-08 19:31:37 +05:00
01zulfi
54d06328c1 web: add tab keyboard shortcuts (#7322)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-08 19:30:20 +05:00
Ammar Ahmed
c74677acd4 mobile: restore settings on logout (#7423) 2025-02-05 09:30:00 +05:00
01zulfi
7689e1a7c5 web: keep some configs in local storage on logout (#7436)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-05 09:29:36 +05:00
luzpaz
5ffae2a08b global: fix various typos (#7463)
Found via `codespell -q 3 -D ../dictionary.txt -S "*.patch,*.po" -L actualy,childs,modifer,ontext,ontop,ony,recieved,reciever,se,seperator,te`

Signed-off-by: Luz Paz <luzpaz@pm.me>
2025-02-05 09:28:06 +05:00
Ammar Ahmed
5fa80abdc8 mobile: update changelog 2025-02-04 19:25:16 +05:00
Ammar Ahmed
a30598fedf mobile: release v3.0.30 2025-02-04 19:23:49 +05:00
Ammar Ahmed
48ffecfb5f mobile: fix e2e tests 2025-02-04 19:23:11 +05:00
Ammar Ahmed
89fe712104 mobile: fix file size is 0 errors when downloading attachments 2025-02-04 17:36:46 +05:00
Ammar Ahmed
5dd298ee86 mobile: fix unlocking note with biometrics 2025-02-04 17:36:32 +05:00
Ammar Ahmed
275b285a8b mobile: release v3.0.29 2025-02-04 12:39:04 +05:00
Ammar Ahmed
96310a46e9 mobile: fix configure note activity not showing biometric unlock prompt 2025-02-04 12:33:05 +05:00
Ammar Ahmed
42f9bf1593 mobile: fix opening widget note on app launch 2025-02-04 12:33:05 +05:00
Ammar Ahmed
25aab6913a mobile: show progress when taking backup and allow to hide backup dialog 2025-02-04 12:32:51 +05:00
Ammar Ahmed
3bd7da68be mobile: fix crash on app launch with new tabs 2025-02-04 12:32:30 +05:00
Abdullah Atta
a218738eff web: improve unfocused window ui 2025-02-04 11:11:51 +05:00
Abdullah Atta
8bf283b3bc config: add *.patch files to .gitignore 2025-02-04 10:14:59 +05:00
luis-411
cb09430ff4 web: fix subnotebook title not updating on navigate (#7286)
Signed-off-by: Luis Kriner <luis@kriner.info>
2025-02-04 10:06:59 +05:00
luis-411
66d75492bb editor: fix task list stats 0/0 on app reload (#7327)
Signed-off-by: Luis Kriner <luis@kriner.info>
2025-02-04 10:04:16 +05:00
Abdullah Atta
d772d38dc7 web: fix collapsed pane expanding on app reload 2025-02-04 09:35:04 +05:00
Ammar Ahmed
46a74f9512 mobile: release v3.0.28 2025-02-03 15:45:54 +05:00
Ammar Ahmed
7df1037e3f mobile: fix realtime sync editor updates in tabs 2025-02-03 15:38:32 +05:00
Abdullah Atta
5c3136d07d ci: run actions on ubuntu-22.04 2025-02-03 14:57:30 +05:00
Abdullah Atta
6a9ddb770e web: bump version to 3.0.25 2025-02-03 14:39:44 +05:00
Abdullah Atta
f4c54fd27f web: fix crash on setting a reminder 2025-02-03 14:22:58 +05:00
Ammar Ahmed
05b12df7d7 mobile: cache password when restoring backup 2025-02-03 14:16:46 +05:00
Ammar Ahmed
dcf053eede mobile: call onClose when closing dialog 2025-02-03 14:16:46 +05:00
Abdullah Atta
5430966b02 web: fix tabs are not clickable everywhere 2025-02-03 13:25:17 +05:00
Ammar Ahmed
7271140627 mobile: fix release notes rendering 2025-02-03 13:02:06 +05:00
01zulfi
eb5ae0773b editor: fix hover styling in toolbar color buttons
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-03 12:36:06 +05:00
Ammar Ahmed
9661dd0c5e mobile: fix android widget bugs 2025-02-03 12:19:53 +05:00
Ammar Ahmed
8e70860918 mobile: fix attaching a file in new note editor 2025-02-03 12:19:34 +05:00
Ammar Ahmed
3540ecf4ab mobile: close sheet on backup completed 2025-02-03 12:19:17 +05:00
Ammar Ahmed
f1dba9ffc2 mobile: cleanup 2025-02-03 12:17:57 +05:00
Abdullah Atta
3f1761a540 core: fix empty note cannot be exported 2025-02-03 12:17:57 +05:00
Abdullah Atta
a25d21038a core: fix Object is not iterable error on some platforms
This happened due to [Symbol.asyncIterable] not getting
transformed when it was a method of a class. Moving it inside
another method fixes the issue.
2025-02-03 12:17:57 +05:00
Abdullah Atta
d55a6a6fc0 web: fix pinned tab not moving to the beginning 2025-02-03 10:46:24 +05:00
Abdullah Atta
e5cf902780 web: fix unnecessary rerenders of the tab strip 2025-02-03 10:01:52 +05:00
Abdullah Atta
a99996fcf6 web: fix tab drag/drop 2025-02-03 09:52:00 +05:00
Abdullah Atta
0cc9c31bc3 editor: fix Failed to execute 'collapse' on 'Selection' 2025-02-01 15:14:34 +05:00
Abdullah Atta
62da8484ad web: ignore notes in other tabs when navigating back 2025-02-01 14:26:12 +05:00
Ammar Ahmed
821b8eebaa mobile: fix tab issues 2025-02-01 13:47:27 +05:00
Ammar Ahmed
269941c297 mobile: keep all datatype classes 2025-02-01 13:47:27 +05:00
Abdullah Atta
e37ff88f1f web: fix flicker when opening a note in new tab 2025-02-01 13:38:48 +05:00
Abdullah Atta
1a1b4bc43c web: fix note already opened in another tab is not focused 2025-02-01 13:18:31 +05:00
Abdullah Atta
2c59df9fb7 web: remove unused code from action-bar 2025-02-01 12:58:53 +05:00
Abdullah Atta
4fc30cc224 web: move focus mode and editor margins to status bar 2025-02-01 12:57:55 +05:00
Abdullah Atta
9bd7d392af web: fix zoom buttons ui 2025-02-01 12:27:51 +05:00
Abdullah Atta
3bcd288bc7 web: more compact and unified action bar 2025-02-01 12:18:47 +05:00
Ammar Ahmed
3b86e51c49 mobile: fix commands 2025-01-31 15:17:45 +05:00
Abdullah Atta
c286b1d595 web: use new storage key for storing editor sessions 2025-01-31 15:15:48 +05:00
Abdullah Atta
2f6b1e6bcc web: fix rapidly clicking on back/forward buttons opened new tab 2025-01-31 15:15:48 +05:00
Abdullah Atta
abb918e179 web: fix tests 2025-01-31 15:15:48 +05:00
Abdullah Atta
dd0dc09977 web: fix note version can't be restored 2025-01-31 15:15:48 +05:00
Abdullah Atta
211ab47165 web: save selection state before switching note 2025-01-31 15:15:48 +05:00
Abdullah Atta
bc44e28e1e web: remove more usages of session.id where noteId was expected 2025-01-31 15:15:48 +05:00
Abdullah Atta
ade60e4272 web: move tab navigation keyboard shortcuts to key-map.ts 2025-01-31 15:15:48 +05:00
Abdullah Atta
3f5bfcb499 web: focus tab if note is already opened in one & reuse sessions for note 2025-01-31 15:15:48 +05:00
Abdullah Atta
782a979397 web: handle case where a new note session is in the tab history 2025-01-31 15:15:48 +05:00
Abdullah Atta
e7f1e2d533 web: add support for opening a session without focusing the tab 2025-01-31 15:15:48 +05:00
Abdullah Atta
12e5f2acc1 web: fix focus next/prev tab not working 2025-01-31 15:15:48 +05:00
Abdullah Atta
dccc79ace3 web: remove usage of session.id in places where note id is expected 2025-01-31 15:15:48 +05:00
Abdullah Atta
a9a5ba09ff web: activate a new note session on tab rehydration 2025-01-31 15:15:48 +05:00
Abdullah Atta
a6c8bbbf0a web: reactivate same session if user tries to create consecutive new notes 2025-01-31 15:15:48 +05:00
Abdullah Atta
e19e9eb96f web: get fresh note context when saving a new note 2025-01-31 15:15:48 +05:00
Ammar Ahmed
9208140b97 mobile: improve tab button 2025-01-31 15:15:48 +05:00
Ammar Ahmed
8241bb8ae5 mobile: fix opening tab for the first time 2025-01-31 15:15:48 +05:00
Ammar Ahmed
329c2e220f mobile: fix tabs 2025-01-31 15:15:48 +05:00
Abdullah Atta
140e343289 common: allow using custom session id in tab session history 2025-01-31 15:15:48 +05:00
Abdullah Atta
0d99367a93 web: fix title not updating in real time sync 2025-01-31 15:15:48 +05:00
Abdullah Atta
b802b3e165 web: fix diff session not opening in editor 2025-01-31 15:15:48 +05:00
Ammar Ahmed
46583e12d9 mobile: update tabs 2025-01-31 15:15:48 +05:00
Abdullah Atta
c9a3f31e8f web: add more tests 2025-01-31 15:15:48 +05:00
Abdullah Atta
53f68aaf81 web: add tests for new tab button 2025-01-31 15:15:48 +05:00
Abdullah Atta
f31df9a6cc web: add tests for new tabs 2025-01-31 15:15:48 +05:00
Abdullah Atta
c812581bb9 web: add support for new tab navigation 2025-01-31 15:15:48 +05:00
Abdullah Atta
1e6e940f17 common: string tab ids, get rid of currentTab taking tab id as a parameter 2025-01-31 15:15:48 +05:00
Ammar Ahmed
9ab670d933 mobile: fix deps 2025-01-31 15:15:48 +05:00
Ammar Ahmed
c46e57418b mobile: remove unused properties 2025-01-31 15:15:48 +05:00
Ammar Ahmed
83557401f5 mobile: update tabs 2025-01-31 15:15:48 +05:00
Ammar Ahmed
2277644e89 mobile: cleanup 2025-01-31 15:15:48 +05:00
Ammar Ahmed
5eb09a1d3e mobile: cleanup 2025-01-31 15:15:48 +05:00
Ammar Ahmed
d957be7a7b mobile: fix editor header ui 2025-01-31 15:15:48 +05:00
Ammar Ahmed
44ddb49d0e editor: fix imports 2025-01-31 15:15:48 +05:00
Ammar Ahmed
24fcd5cd0c common: fix missing exports 2025-01-31 15:15:48 +05:00
Ammar Ahmed
2a34225d49 global: update deps 2025-01-31 15:15:48 +05:00
Ammar Ahmed
e81c825fc7 mobile: multi-tab support 2025-01-31 15:15:48 +05:00
Ammar Ahmed
c80286b587 mobile: tab history 2025-01-31 15:15:48 +05:00
luis-411
e85f8b60b0 editor: add field labels to add a link popup (#7097)
Signed-off-by: Luis Kriner <luis@kriner.info>
2025-01-31 10:11:59 +05:00
luis-411
5daa99d969 web: fix nav menu collapse (#7377)
Signed-off-by: Luis Kriner <luis@kriner.info>
2025-01-30 09:09:28 +05:00
132 changed files with 3960 additions and 3645 deletions

View File

@@ -18,7 +18,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4

View File

@@ -37,7 +37,7 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@@ -14,7 +14,7 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -133,7 +133,7 @@ jobs:
test-linux:
name: Test for Linux
needs: build
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Check out Git repository

View File

@@ -19,7 +19,7 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@@ -11,7 +11,7 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@@ -14,7 +14,7 @@ on: workflow_dispatch
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
permissions:
packages: write
contents: read

View File

@@ -5,7 +5,7 @@ on: workflow_dispatch
jobs:
build:
name: Build
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@@ -5,7 +5,7 @@ on: workflow_dispatch
jobs:
build:
name: Build
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@@ -14,7 +14,7 @@ on:
jobs:
bench:
name: Bench
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@@ -5,7 +5,7 @@ on: workflow_dispatch
jobs:
build:
name: Build
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@@ -19,7 +19,7 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ nx-cloud.env
site
node_modules.backup
.nx
*.patch

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.0.24",
"version": "3.0.25",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.0.24",
"version": "3.0.25",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.24",
"version": "3.0.25",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -39,7 +39,6 @@ import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import { IntentService } from "./services/intent";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
@@ -49,7 +48,6 @@ if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
IntentService.onLaunch();
const App = (props: { configureMode: "note-preview" }) => {
useAppEvents();
//@ts-ignore

View File

@@ -115,7 +115,7 @@ export const FileSizeResult = {
Error: -1
};
export async function getUploadedFileSize(hash: string) {
export async function getUploadedFileSize(hash: string, retry = 0) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
@@ -123,8 +123,24 @@ export async function getUploadedFileSize(hash: string) {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
if (
!attachmentInfo.ok ||
attachmentInfo.headers?.get("content-length") === null
) {
if (retry < 3) {
DatabaseLogger.log(`Retrying file size check: ${hash}, ${retry}`);
return getUploadedFileSize(hash, retry + 1);
}
throw new Error(
`File size check failed: ${hash}, ${
attachmentInfo.status
}, ${attachmentInfo.headers?.get("content-length")}`
);
}
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length") || "0"
attachmentInfo.headers?.get("content-length") as string
);
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
} catch (e) {

View File

@@ -112,14 +112,19 @@ const AppLockedOverlay = () => {
if (
!biometricsAuthEnabled ||
!(await BiometricService.isBiometryAvailable())
)
) {
return;
}
biometricUnlockAwaitingUserInput.current = true;
if (Platform.OS === "android") {
const activityName = await NotesnookModule.getActivityName();
if (activityName !== "MainActivity") return;
if (
activityName !== "MainActivity" &&
activityName !== "NotePreviewConfigureActivity"
)
return;
}
useSettingStore.getState().setRequestBiometrics(true);
@@ -180,7 +185,6 @@ const AppLockedOverlay = () => {
DatabaseLogger.info("Locking app on entering foreground");
useUserStore.getState().lockApp(true);
}
if (
!(
biometricUnlockAwaitingUserInput.current ||

View File

@@ -182,18 +182,17 @@ const Actions = ({
relations
.map((relation) => relation.fromId)
.forEach(async (id) => {
const tab = useTabStore.getState().getTabForNote(id);
if (tab !== undefined) {
const isFocused = useTabStore.getState().currentTab === tab;
useTabStore.getState().forEachNoteTab(id, async (tab) => {
const isFocused = useTabStore.getState().currentTab === tab.id;
if (isFocused) {
eSendEvent(eOnLoadNote, {
item: await db.notes.note(id),
forced: true
});
} else {
editorController.current.commands.setLoading(true, tab);
editorController.current.commands.setLoading(true, tab.id);
}
}
});
});
close?.();
},

View File

@@ -110,6 +110,7 @@ export const Dialog = ({ context = "global" }) => {
setChecked(false);
values.current.inputValue = undefined;
setVisible(false);
dialogInfo.onClose();
};
const onNegativePress = async () => {

View File

@@ -76,7 +76,8 @@ const NoteItem = ({
}: NoteItemProps) => {
const isEditingNote = useTabStore(
(state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
state.tabs.find((t) => t.id === state.currentTab)?.session?.noteId ===
item.id
);
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(

View File

@@ -28,7 +28,8 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
const { colors } = useThemeColors();
const isEditingNote = useTabStore(
(state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
state.tabs.find((t) => t.id === state.currentTab)?.session?.noteId ===
item.id
);
const [selected] = useIsSelected(item);

View File

@@ -141,7 +141,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
close();
eSendEvent(eOnLoadNote, {
item: item,
presistTab: true
newTab: true
});
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);

View File

@@ -16,8 +16,8 @@ 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 { Note } from "@notesnook/core";
import { EVENTS } from "@notesnook/core";
import { EVENTS, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
@@ -25,27 +25,18 @@ import { FlatList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import {
TabItem,
useTabStore
} from "../../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../../screens/editor/tiptap/utils";
import { eSendEvent, presentSheet } from "../../../services/event-manager";
import { eUnlockNote } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
type TabItem = {
id: number;
noteId?: string;
previewTab?: boolean;
locked?: boolean;
noteLocked?: boolean;
readonly?: boolean;
pinned?: boolean;
};
const TabItemComponent = (props: {
tab: TabItem;
@@ -53,7 +44,7 @@ const TabItemComponent = (props: {
close?: (ctx?: string | undefined) => void;
}) => {
const { colors } = useThemeColors();
const [item, update] = useDBItem(props.tab.noteId, "note");
const [item, update] = useDBItem(props.tab.session?.noteId, "note");
useEffect(() => {
const syncCompletedSubscription = db.eventManager?.subscribe(
@@ -85,11 +76,11 @@ const TabItemComponent = (props: {
onPress={() => {
if (!props.isFocused) {
useTabStore.getState().focusTab(props.tab.id);
if (props.tab.locked) {
if (props.tab.session?.locked) {
eSendEvent(eUnlockNote);
}
if (!props.tab.noteId) {
if (!props.tab.session?.noteId) {
setTimeout(() => {
editorController?.current?.commands?.focus(props.tab.id);
}, 300);
@@ -97,11 +88,6 @@ const TabItemComponent = (props: {
}
props.close?.();
}}
onLongPress={() => {
useTabStore.getState().updateTab(props.tab.id, {
previewTab: false
});
}}
>
<View
style={{
@@ -112,9 +98,9 @@ const TabItemComponent = (props: {
flexShrink: 1
}}
>
{props.tab.noteLocked ? (
{props.tab.session?.noteLocked ? (
<>
{props.tab.locked ? (
{props.tab.session?.locked ? (
<Icon size={SIZE.md} name="lock" />
) : (
<Icon size={SIZE.md} name="lock-open-outline" />
@@ -122,7 +108,9 @@ const TabItemComponent = (props: {
</>
) : null}
{props.tab.readonly ? <Icon size={SIZE.md} name="pencil-lock" /> : null}
{props.tab.session?.readonly ? (
<Icon size={SIZE.md} name="pencil-lock" />
) : null}
<Paragraph
color={
@@ -130,15 +118,10 @@ const TabItemComponent = (props: {
? colors.selected.paragraph
: colors.primary.paragraph
}
style={{
fontFamily: props.tab.previewTab
? "OpenSans-Italic"
: "OpenSans-Regular"
}}
numberOfLines={1}
size={SIZE.md}
>
{props.tab.noteId
{props.tab.session?.noteId
? item?.title || strings.untitledNote()
: strings.newNote()}
</Paragraph>
@@ -157,8 +140,7 @@ const TabItemComponent = (props: {
color={props.tab.pinned ? colors.primary.accent : colors.primary.icon}
onPress={() => {
useTabStore.getState().updateTab(props.tab.id, {
pinned: !props.tab.pinned,
previewTab: false
pinned: !props.tab.pinned
});
}}
top={0}
@@ -197,6 +179,7 @@ export default function EditorTabs({
}: {
close?: (ctx?: string | undefined) => void;
}) {
const { colors } = useThemeColors();
const [tabs, currentTab] = useTabStore((state) => [
state.tabs,
state.currentTab
@@ -233,25 +216,13 @@ export default function EditorTabs({
}}
>
<Heading size={SIZE.lg}>{strings.tabs()}</Heading>
<Button
<IconButton
onPress={() => {
useTabStore.getState().newTab();
setTimeout(() => {
editorController?.current?.commands?.focus(
useTabStore.getState().currentTab
);
}, 500);
close?.();
}}
title={strings.newTab()}
icon="plus"
style={{
flexDirection: "row",
justifyContent: "flex-start",
borderRadius: 100,
height: 35
}}
iconSize={SIZE.lg}
name="plus"
color={colors.primary.accent}
/>
</View>

View File

@@ -22,24 +22,22 @@ import {
VirtualizedGrouping,
createInternalLink
} from "@notesnook/core";
import type { LinkAttributes } from "@notesnook/editor";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item";
import { editorController } from "../../../screens/editor/tiptap/utils";
import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import type { LinkAttributes } from "@notesnook/editor";
import {
EditorEvents,
editorController
} from "../../../screens/editor/tiptap/utils";
import { strings } from "@notesnook/intl";
const ListNoteItem = ({
id,
@@ -194,7 +192,7 @@ export default function LinkNote(props: {
}
: undefined
);
editorController.current?.postMessage(EditorEvents.resolve, {
editorController.current?.postMessage(NativeEvents.resolve, {
data: {
href: link,
title: selectedNote.title

View File

@@ -43,7 +43,7 @@ export const Update = ({ version: appVersion, fwdRef }) => {
let notes = version?.notes
? version.notes.replace("Thank you for using Notesnook!", "").split("- ")
: ["Bug fixes and performance improvements"];
notes = notes?.map((n) => n.replace(/\n/g, ""));
notes = notes?.map((n) => n.replace(/\n|<br>/g, ""));
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const getSupportedAbi = () => {
@@ -178,18 +178,20 @@ export const Update = ({ version: appVersion, fwdRef }) => {
{version.body}
</Paragraph>
) : null}
{notes.map((item) => (
<Paragraph
key={item}
color={colors.secondary.paragraph}
style={{
marginBottom: 5
}}
selectable
>
{item}
</Paragraph>
))}
{notes.map((item) =>
item && item !== "" ? (
<Paragraph
key={item}
color={colors.secondary.paragraph}
style={{
marginBottom: 5
}}
selectable
>
{item}
</Paragraph>
) : null
)}
</ScrollView>
<Seperator />
<Button

View File

@@ -19,14 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable no-inner-declarations */
import {
Color,
createInternalLink,
ItemReference,
Note,
Notebook,
Reminder,
Tag,
TrashItem,
VAULT_ERRORS,
createInternalLink
VAULT_ERRORS
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { DisplayedNotification } from "@notifee/react-native";
@@ -50,11 +50,11 @@ import ReminderSheet from "../components/sheets/reminder";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
openVault,
presentSheet
presentSheet,
ToastManager
} from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
@@ -544,14 +544,13 @@ export const useActions = ({
const toggleReadyOnlyMode = async () => {
const currentReadOnly = (item as Note).readonly;
await db.notes.readonly(!currentReadOnly, item?.id);
if (useTabStore.getState().hasTabForNote(item.id)) {
const tabId = useTabStore.getState().getTabForNote(item.id);
if (!tabId) return;
useTabStore.getState().updateTab(tabId, {
readonly: !currentReadOnly
useTabStore.getState().forEachNoteTab(item.id, (tab) => {
useTabStore.getState().updateTab(tab.id, {
session: {
readonly: !currentReadOnly
}
});
}
});
Navigation.queueRoutesForUpdate();
close();
};

View File

@@ -50,12 +50,17 @@ import { MMKV } from "../common/database/mmkv";
import { endProgress, startProgress } from "../components/dialogs/progress";
import Migrate from "../components/sheets/migrate";
import NewFeature from "../components/sheets/new-feature";
import ReminderSheet from "../components/sheets/reminder";
import { Walkthrough } from "../components/walkthroughs";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import {
resetTabStore,
useTabStore
} from "../screens/editor/tiptap/use-tab-store";
import {
clearAppState,
editorController,
editorState
editorState,
setAppState
} from "../screens/editor/tiptap/utils";
import { useDragState } from "../screens/settings/editor/state";
import BackupService from "../services/backup";
@@ -66,7 +71,6 @@ import {
eSubscribeEvent,
presentSheet
} from "../services/event-manager";
import { IntentService } from "../services/intent";
import {
clearMessage,
setEmailVerifyMessage,
@@ -95,7 +99,6 @@ import {
eLoginSessionExpired,
eOnLoadNote,
eOpenAnnouncementDialog,
eUnlockNote,
eUserLoggedIn,
refreshNotesPage
} from "../utils/events";
@@ -157,6 +160,7 @@ const onUserSessionExpired = async () => {
const onAppOpenedFromURL = async (event: { url: string }) => {
const url = event.url;
try {
if (url.startsWith("https://app.notesnook.com/account/verified")) {
await onUserEmailVerified();
@@ -166,6 +170,25 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
eSendEvent(eOnLoadNote, { newNote: true });
tabBarRef.current?.goToPage(1, false);
return;
} else if (url.startsWith("https://notesnook.com/open_note")) {
const id = new URL(url).searchParams.get("id");
if (id) {
const note = await db.notes.note(id);
if (note) {
eSendEvent(eOnLoadNote, {
item: note
});
tabBarRef.current?.goToPage(1, false);
}
}
} else if (url.startsWith("https://notesnook.com/open_reminder")) {
const id = new URL(url).searchParams.get("id");
if (id) {
const reminder = await db.reminders.reminder(id);
if (reminder) ReminderSheet.present(reminder);
}
} else if (url.startsWith("https://notesnook.com/new_reminder")) {
ReminderSheet.present();
}
} catch (e) {
console.error(e);
@@ -222,6 +245,7 @@ const onLogout = async (reason: string) => {
await PremiumService.setPremiumStatus();
await BiometricService.resetCredentials();
MMKV.clearStore();
resetTabStore();
clearAllStores();
setImmediate(() => {
refreshAllStores();
@@ -620,22 +644,17 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.vaultLocked, async () => {
// Lock all notes in all tabs...
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
if (!noteId) continue;
const note = await db.notes.note(noteId);
const locked = note && (await db.vaults.itemExists(note));
if (locked) {
useTabStore.getState().updateTab(tab.id, {
locked: true
session: {
locked: true,
noteLocked: true
}
});
if (
tab.id === useTabStore.getState().currentTab &&
locked &&
!editorState().movedAway
) {
// Show unlock note screen.
eSendEvent(eUnlockNote);
}
}
}
}),
@@ -695,10 +714,6 @@ export const useAppEvents = () => {
// Reset the editor if the app has been in background for more than 10 minutes.
eSendEvent(eEditorReset);
}
setTimeout(async () => {
IntentService.onAppStateChanged();
}, 100);
} else {
await saveEditorState();
if (
@@ -734,13 +749,15 @@ export const useAppEvents = () => {
setTimeout(() => {
sub = AppState.addEventListener("change", onAppStateChanged);
if (
refValues.current.initialUrl?.startsWith(
"https://app.notesnook.com/account/verified"
)
refValues.current.initialUrl &&
!refValues.current.initialUrl?.includes("open_note")
) {
onUserEmailVerified();
onAppOpenedFromURL({
url: refValues.current.initialUrl!
});
}
}, 1000);
refValues.current.removeInternetStateListener = NetInfo.addEventListener(
onInternetStateChanged
);

View File

@@ -17,6 +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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
@@ -44,10 +45,6 @@ import useNavigationStore from "../stores/use-navigation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { useSettingStore } from "../stores/use-setting-store";
import { rootNavigatorRef } from "../utils/global-refs";
import { strings } from "@notesnook/intl";
import { IntentService } from "../services/intent";
import ReminderSheet from "../components/sheets/reminder";
import { db } from "../common/database";
const NativeStack = createNativeStackNavigator();
const IntroStack = createNativeStackNavigator();
@@ -86,17 +83,6 @@ const _Tabs = () => {
React.useEffect(() => {
setTimeout(async () => {
useNavigationStore.getState().update(homepage);
const intent = IntentService.getLaunchIntent();
if (intent && intent["com.streetwriters.notesnook.OpenReminderId"]) {
const reminder = await db.reminders.reminder(
intent["com.streetwriters.notesnook.OpenReminderId"]
);
if (reminder) {
ReminderSheet.present(reminder);
}
} else if (intent["com.streetwriters.notesnook.NewReminder"]) {
ReminderSheet.present();
}
}, 1000);
}, [homepage]);

View File

@@ -509,7 +509,8 @@ const onChangeTab = async (event) => {
});
} else {
if (
useTabStore.getState().getTab(useTabStore.getState().currentTab).locked
useTabStore.getState().getTab(useTabStore.getState().currentTab).session
?.locked
) {
eSendEvent(eUnlockNote);
}
@@ -523,13 +524,15 @@ const onChangeTab = async (event) => {
// Lock all tabs with locked notes...
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
if (!noteId) continue;
const note = await db.notes.note(noteId);
const locked = note && (await db.vaults.itemExists(note));
if (locked) {
useTabStore.getState().updateTab(tab.id, {
locked: true
session: {
locked: true
}
});
}
}

View File

@@ -39,7 +39,12 @@
"@lingui/core": "5.1.2",
"@lingui/react": "5.1.2",
"react-native-check-version": "^1.3.0",
"react-native-material-menu": "^2.0.0"
"react-native-material-menu": "^2.0.0",
"@trpc/client": "^10.45.2",
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"@tanstack/react-query": "^4.36.1",
"async-mutex": "0.5.0"
},
"sideEffects": false
}

View File

@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable @typescript-eslint/no-var-requires */
import { i18n } from "@lingui/core";
import { strings } from "@notesnook/intl";
import React, {
forwardRef,
useCallback,
@@ -46,6 +48,7 @@ import {
eUnlockWithPassword
} from "../../utils/events";
import { openLinkInBrowser } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import EditorOverlay from "./loading";
import { EDITOR_URI } from "./source";
import { EditorProps, useEditorType } from "./tiptap/types";
@@ -58,9 +61,6 @@ import {
openInternalLink,
randId
} from "./tiptap/utils";
import { tabBarRef } from "../../utils/global-refs";
import { strings } from "@notesnook/intl";
import { i18n } from "@lingui/core";
const style: ViewStyle = {
height: "100%",
@@ -203,11 +203,13 @@ const useLockedNoteHandler = () => {
useEffect(() => {
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
if (!noteId) continue;
if (tabRef.current && tabRef.current.noteLocked) {
if (tabRef.current && tabRef.current.session?.noteLocked) {
useTabStore.getState().updateTab(tabRef.current.id, {
locked: true
session: {
locked: true
}
});
}
}
@@ -221,33 +223,38 @@ const useLockedNoteHandler = () => {
biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint
});
syncTabs();
syncTabs("biometry");
})();
}, [tab?.id]);
useEffect(() => {
const unlockWithBiometrics = async () => {
try {
if (!tabRef.current?.noteLocked || !tabRef.current) return;
if (!tabRef.current?.session?.noteLocked || !tabRef.current) return;
console.log("Trying to unlock with biometrics...");
const credentials = await BiometricService.getCredentials(
"Unlock note",
"Unlock note to open it in editor."
);
if (credentials && credentials?.password && tabRef.current.noteId) {
if (
credentials &&
credentials?.password &&
tabRef.current.session?.noteId
) {
const note = await db.vault.open(
tabRef.current.noteId,
tabRef.current.session?.noteId,
credentials?.password
);
eSendEvent(eOnLoadNote, {
item: note
});
useTabStore.getState().updateTab(tabRef.current.id, {
locked: false
item: note,
refresh: true
});
} else {
if (tabRef.current && tabRef.current.session?.locked) {
editorController.current?.commands.focusPassInput();
}
}
} catch (e) {
console.error(e);
@@ -261,7 +268,7 @@ const useLockedNoteHandler = () => {
password: string;
biometrics?: boolean;
}) => {
if (!tabRef.current?.noteId || !tabRef.current) return;
if (!tabRef.current?.session?.noteId || !tabRef.current) return;
if (!password || password.trim().length === 0) {
ToastManager.show({
heading: strings.passwordNotEntered(),
@@ -271,7 +278,10 @@ const useLockedNoteHandler = () => {
}
try {
const note = await db.vault.open(tabRef.current?.noteId, password);
const note = await db.vault.open(
tabRef.current?.session?.noteId,
password
);
if (enrollBiometrics && note) {
try {
const unlocked = await db.vault.unlock(password);
@@ -299,10 +309,8 @@ const useLockedNoteHandler = () => {
}
}
eSendEvent(eOnLoadNote, {
item: note
});
useTabStore.getState().updateTab(tabRef.current.id, {
locked: false
item: note,
refresh: true
});
} catch (e) {
ToastManager.show({
@@ -314,7 +322,7 @@ const useLockedNoteHandler = () => {
const unlock = () => {
if (
(tabRef.current?.locked,
(tabRef.current?.session?.locked,
useTabStore.getState().biometryAvailable &&
useTabStore.getState().biometryEnrolled &&
!editorState().movedAway)
@@ -325,7 +333,7 @@ const useLockedNoteHandler = () => {
} else {
if (!editorState().movedAway) {
setTimeout(() => {
if (tabRef.current && tabRef.current?.locked) {
if (tabRef.current && tabRef.current?.session?.locked) {
editorController.current?.commands.focus(tabRef.current?.id);
}
}, 100);
@@ -340,13 +348,13 @@ const useLockedNoteHandler = () => {
}),
eSubscribeEvent(eUnlockWithPassword, onSubmit)
];
if (tabRef.current?.locked && tabBarRef.current?.page() === 2) {
if (tabRef.current?.session?.locked && tabBarRef.current?.page() === 2) {
unlock();
}
return () => {
subs.map((s) => s?.unsubscribe());
};
}, [tab?.id, tab?.locked]);
}, [tab?.id, tab?.session?.locked]);
return null;
};

View File

@@ -27,10 +27,10 @@ import WebView from "react-native-webview";
import { useRef } from "react";
import { EDITOR_URI } from "./source";
import { EditorMessage } from "./tiptap/types";
import { EventTypes } from "./tiptap/editor-events";
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
import { Attachment } from "@notesnook/editor";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { EditorEvents } from "./tiptap/utils";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { useThemeColors } from "@notesnook/theme";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { db } from "../../common/database";
@@ -69,11 +69,11 @@ export function ReadonlyEditor(props: {
const data = event.nativeEvent.data;
const editorMessage = JSON.parse(data) as EditorMessage<any>;
if (editorMessage.type === EventTypes.logger) {
if (editorMessage.type === EditorEvents.logger) {
logger.info("[READONLY EDITOR LOG]", editorMessage.value);
}
if (editorMessage.type === EventTypes.readonlyEditorLoaded) {
if (editorMessage.type === EditorEvents.readonlyEditorLoaded) {
props.onLoad?.((content: { data: string; id: string }) => {
setTimeout(() => {
noteId.current = content.id;
@@ -86,7 +86,7 @@ export function ReadonlyEditor(props: {
setLoading(false);
}, 300);
});
} else if (editorMessage.type === EventTypes.getAttachmentData) {
} else if (editorMessage.type === EditorEvents.getAttachmentData) {
const attachment = (editorMessage.value as any).attachment as Attachment;
downloadAttachment(attachment.hash, true, {
@@ -104,7 +104,7 @@ export function ReadonlyEditor(props: {
);
editorRef.current?.postMessage(
JSON.stringify({
type: EditorEvents.attachmentData,
type: NativeEvents.attachmentData,
value: {
resolverId: (editorMessage.value as any).resolverId,
data
@@ -115,7 +115,7 @@ export function ReadonlyEditor(props: {
.catch(() => {
editorRef.current?.postMessage(
JSON.stringify({
type: EditorEvents.attachmentData,
type: NativeEvents.attachmentData,
data: {
resolverId: (editorMessage.value as any).resolverId,
data: undefined

View File

@@ -18,9 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Note } from "@notesnook/core";
import type { Attachment } from "@notesnook/editor";
import type { ImageAttributes } from "@notesnook/editor";
import type { LinkAttributes } from "@notesnook/editor";
import type {
Attachment,
ImageAttributes,
LinkAttributes
} from "@notesnook/editor";
import { createRef, RefObject } from "react";
import { Platform } from "react-native";
import { EdgeInsets } from "react-native-safe-area-context";
@@ -37,9 +39,6 @@ async function call(webview: RefObject<WebView | undefined>, action?: Action) {
if (!webview.current || !action) return;
setImmediate(() => webview.current?.injectJavaScript(action.job));
const response = await getResponse(action.id);
// if (!response) {
// console.warn("webview job failed", action.id);
// }
return response ? response.value : response;
}
@@ -75,110 +74,71 @@ class Commands {
return call(this.ref, fn(job, name)) as Promise<T>;
}
focus = async (tabId: number) => {
async sendCommand<T>(command: string, ...args: any[]) {
return this.doAsync(
`response = globalThis.commands.${command}(${args
.map((arg) =>
typeof arg === "string" ? `"${arg}"` : JSON.stringify(arg)
)
.join(",")})`,
command
);
}
focus = async (tabId: string) => {
if (!this.ref.current) return;
const locked = useTabStore.getState().getTab(tabId)?.session?.locked;
if (Platform.OS === "android") {
//this.ref.current?.requestFocus();
setTimeout(async () => {
if (
locked &&
useTabStore.getState().biometryAvailable &&
useTabStore.getState().biometryEnrolled
)
return;
if (!this.ref) return;
textInput.current?.focus();
const locked = useTabStore.getState().getTab(tabId)?.locked;
await this.doAsync(
locked
? `editorControllers[${tabId}]?.focusPassInput();`
: `editors[${tabId}]?.commands.focus()`,
"focus"
);
await this.sendCommand("focus", tabId, locked);
this.ref?.current?.requestFocus();
}, 1);
} else {
await sleep(400);
await this.doAsync(`editors[${tabId}]?.commands.focus()`, "focus");
await this.sendCommand("focus", tabId, locked);
}
};
blur = async (tabId: number) =>
await this.doAsync(
`
const editor = editors[${tabId}];
const editorTitle = editorTitles[${tabId}];
typeof editor !== "undefined" && editor.commands.blur();
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current.blur();
editorControllers[${tabId}]?.blurPassInput();
blur = async (tabId: string) => this.sendCommand("blur", tabId);
`,
"blur"
);
clearContent = async (tabId: number) => {
clearContent = async (tabId: string) => {
this.previousSettings = null;
await this.doAsync(
`
const editor = editors[${tabId}];
const editorController = editorControllers[${tabId}];
const editorTitle = editorTitles[${tabId}];
const statusBar = statusBars[${tabId}];
if (typeof editor !== "undefined") {
editor.commands.blur();
editor.commands.clearContent(false);
}
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current?.blur();
if (typeof editorController.content !== undefined) editorController.content.current = '';
editorController.onUpdate();
editorController.setTitle('');
if (typeof statusBar !== "undefined") {
statusBar.current.resetWords();
statusBar.current.set({date:"",saved:""});
}`,
"clearContent"
);
await this.sendCommand("clearContent", tabId);
};
setSessionId = async (id: string | null) =>
await this.doAsync(`globalThis.sessionId = "${id}";`);
await this.sendCommand("setSessionId", id);
setStatus = async (
date: string | undefined,
saved: string,
tabId: number
tabId: string
) => {
await this.doAsync(
`
const statusBar = statusBars[${tabId}];
typeof statusBar !== "undefined" && statusBar.current.set({date:"${date}",saved:"${saved}"})`,
"setStatus"
this.sendCommand("setStatus", date, saved, tabId);
};
setPlaceholder = async (placeholder: string) => {};
setLoading = async (loading?: boolean, tabId?: string) => {
this.sendCommand(
"setLoading",
loading,
tabId === undefined ? useTabStore.getState().currentTab : tabId
);
};
setPlaceholder = async (placeholder: string) => {
// await this.doAsync(`
// const element = document.querySelector(".is-editor-empty");
// if (element) {
// element.setAttribute("data-placeholder","${placeholder}");
// }
// `);
};
setLoading = async (loading?: boolean, tabId?: number) => {
await this.doAsync(`
const editorController = editorControllers[${
tabId || useTabStore.getState().currentTab
}];
editorController.setLoading(${loading})
`);
};
setInsets = async (insets: EdgeInsets) => {
await this.doAsync(`
if (typeof safeAreaController !== "undefined") {
safeAreaController.update(${JSON.stringify(insets)})
}
`);
this.sendCommand("setInsets", insets);
};
updateSettings = async (settings?: Partial<Settings>) => {
@@ -187,13 +147,7 @@ if (typeof statusBar !== "undefined") {
...this.previousSettings,
...settings
};
await this.doAsync(`
if (typeof globalThis.settingsController !== "undefined") {
globalThis.settingsController.update(${JSON.stringify(
this.previousSettings
)})
}
`);
this.sendCommand("updateSettings", settings);
};
setSettings = async (settings?: Partial<Settings>) => {
@@ -206,123 +160,63 @@ if (typeof statusBar !== "undefined") {
return;
}
}
await this.doAsync(`
if (typeof globalThis.settingsController !== "undefined") {
globalThis.settingsController.update(${JSON.stringify(settings)})
}
`);
this.sendCommand("setSettings", settings);
};
setTags = async (note: Note | null | undefined) => {
if (!note) return;
const tabId = useTabStore.getState().getTabForNote(note.id);
const tags = await db.relations.to(note, "tag").resolve();
await this.doAsync(
`
const tags = editorTags[${tabId}];
if (tags && tags.current) {
tags.current.setTags(${JSON.stringify(
tags.map((tag) => ({
title: tag.title,
alias: tag.title,
id: tag.id,
type: tag.type
}))
)});
}
`,
"setTags"
);
useTabStore.getState().forEachNoteTab(note.id, async (tab) => {
const tabId = tab.id;
const tags = await db.relations.to(note, "tag").resolve();
await this.sendCommand("setTags", tabId, tags);
});
};
clearTags = async (tabId: number) => {
await this.doAsync(
`
const tags = editorTags[${tabId}];
logger("info", Object.keys(editorTags), typeof editorTags[0]);
if (tags && tags.current) {
tags.current.setTags([]);
}
`,
"clearTags"
);
clearTags = async (tabId: string) => {
await this.sendCommand("clearTags", tabId);
};
insertAttachment = async (attachment: Attachment, tabId: number) => {
await this.doAsync(
`const editor = editors[${tabId}];
editor && editor.commands.insertAttachment(${JSON.stringify(attachment)})`
);
insertAttachment = async (attachment: Attachment, tabId: string) => {
await this.sendCommand("insertAttachment", attachment, tabId);
};
setAttachmentProgress = async (
attachmentProgress: Partial<Attachment>,
tabId: number
tabId: string
) => {
await this.doAsync(
`const editor = editors[${tabId}];
editor && editor.commands.updateAttachment(${JSON.stringify(
attachmentProgress
)}, {
preventUpdate: true,
query: (attachment) => {
return attachment.hash === "${attachmentProgress.hash}";
}
})`
);
await this.sendCommand("setAttachmentProgress", attachmentProgress, tabId);
};
insertImage = async (
image: Omit<ImageAttributes, "bloburl"> & {
dataurl: string;
},
tabId: number
tabId: string
) => {
await this.doAsync(
`const editor = editors[${tabId}];
const image = toBlobURL("${image.dataurl}", "${image.hash}");
editor && editor.commands.insertImage({
...${JSON.stringify({
...image,
dataurl: undefined
})},
bloburl: image
})`
);
await this.sendCommand("insertImage", image, tabId);
};
handleBack = async () => {
return this.doAsync<boolean>(
'response = window.dispatchEvent(new Event("handleBackPress",{cancelable:true}));'
);
return this.sendCommand("handleBack");
};
keyboardShown = async (keyboardShown: boolean) => {
return this.doAsync(`globalThis['keyboardShown']=${keyboardShown};`);
return this.sendCommand("keyboardShown", keyboardShown);
};
getTableOfContents = async () => {
const tabId = useTabStore.getState().currentTab;
return this.doAsync(`
response = editorControllers[${tabId}]?.getTableOfContents() || [];
`);
return this.sendCommand("getTableOfContents", tabId);
};
focusPassInput = async () => {
const tabId = useTabStore.getState().currentTab;
return this.doAsync(`
response = editorControllers[${tabId}]?.focusPassInput() || [];
`);
return this.sendCommand("focusPassInput", tabId);
};
blurPassInput = async () => {
const tabId = useTabStore.getState().currentTab;
return this.doAsync(`
response = editorControllers[${tabId}]?.blurPassInput() || [];
`);
return this.sendCommand("blurPassInput", tabId);
};
createInternalLink = async (
@@ -330,30 +224,18 @@ editor && editor.commands.insertImage({
resolverId: string
) => {
if (!resolverId) return;
return this.doAsync(`
if (globalThis.pendingResolvers["${resolverId}"]) {
globalThis.pendingResolvers["${resolverId}"](${JSON.stringify(
attributes
)});
}`);
return this.sendCommand("createInternalLink", attributes, resolverId);
};
dismissCreateInternalLinkRequest = async (resolverId: string) => {
if (!resolverId) return;
return this.doAsync(`
if (globalThis.pendingResolvers["${resolverId}"]) {
globalThis.pendingResolvers["${resolverId}"](undefined);
}
`);
return this.sendCommand("dismissCreateInternalLinkRequest", resolverId);
};
scrollIntoViewById = async (id: string) => {
const tabId = useTabStore.getState().currentTab;
return this.doAsync(`
response = editorControllers[${tabId}]?.scrollIntoView("${id}") || [];
`);
return this.sendCommand("scrollIntoViewById", id, tabId);
};
//todo add replace image function
}
export default Commands;

View File

@@ -61,7 +61,7 @@ const santizeUri = (uri: string) => {
type PickerOptions = {
noteId?: string;
tabId?: number;
tabId?: string;
type: "image" | "camera" | "file";
reupload: boolean;
hash?: string;

View File

@@ -74,7 +74,7 @@ export type EditorMessage<T> = {
value: T;
type: string;
noteId: string;
tabId: number;
tabId: string;
resolverId?: string;
hasTimeout?: boolean;
};
@@ -86,7 +86,7 @@ export type SavePayload = {
type?: "tiptap";
sessionHistoryId?: number;
ignoreEdit: boolean;
tabId: number;
tabId: string;
pendingChanges?: boolean;
};

View File

@@ -21,7 +21,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable @typescript-eslint/no-var-requires */
import { ItemReference } from "@notesnook/core";
import type { Attachment } from "@notesnook/editor";
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { getDefaultPresets } from "@notesnook/editor/dist/cjs/toolbar/tool-definitions";
import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useRef } from "react";
import {
@@ -72,11 +75,9 @@ import {
import { openLinkInBrowser } from "../../../utils/functions";
import { tabBarRef } from "../../../utils/global-refs";
import { useDragState } from "../../settings/editor/state";
import { EventTypes } from "./editor-events";
import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { EditorEvents, editorState, openInternalLink } from "./utils";
import { strings } from "@notesnook/intl";
import { editorState, openInternalLink } from "./utils";
const publishNote = async () => {
const user = useUserStore.getState().user;
@@ -101,7 +102,7 @@ const publishNote = async () => {
}
const noteId = useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab);
.getNoteIdForTab(useTabStore.getState().currentTab!);
if (noteId) {
const note = await db.notes?.note(noteId);
@@ -124,7 +125,7 @@ const publishNote = async () => {
const showActionsheet = async () => {
const noteId = useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab);
.getNoteIdForTab(useTabStore.getState().currentTab!);
if (noteId) {
const note = await db.notes?.note(noteId);
if (editorState().isFocused || editorState().isFocused) {
@@ -177,7 +178,7 @@ export const useEditorEvents = (
useEffect(() => {
const handleKeyboardDidShow: KeyboardEventListener = () => {
editor.commands.keyboardShown(true);
editor.postMessage(EditorEvents.keyboardShown, undefined);
editor.postMessage(NativeEvents.keyboardShown, undefined);
};
const handleKeyboardDidHide: KeyboardEventListener = () => {
editor.commands.keyboardShown(false);
@@ -247,7 +248,7 @@ export const useEditorEvents = (
}
editorState().currentlyEditing = false;
// editor.reset(); Notes remain open.
editor.commands?.blur(useTabStore.getState().currentTab);
editor.commands?.blur(useTabStore.getState().currentTab!);
setTimeout(async () => {
if (deviceMode !== "mobile" && fullscreen) {
if (fullscreen) {
@@ -352,25 +353,25 @@ export const useEditorEvents = (
const editorMessage = JSON.parse(data) as EditorMessage<any>;
if (editorMessage.hasTimeout && editorMessage.resolverId) {
editor.postMessage(EditorEvents.resolve, {
editor.postMessage(NativeEvents.resolve, {
data: true,
resolverId: editorMessage.resolverId
});
}
if (editorMessage.type === EventTypes.load) {
if (editorMessage.type === EditorEvents.load) {
DatabaseLogger.log("Editor is ready");
editor.onLoad();
return;
}
if (editorMessage.type === EventTypes.back) {
if (editorMessage.type === EditorEvents.back) {
return onBackPress();
}
if (
editorMessage.sessionId !== editor.sessionId.current &&
editorMessage.type !== EditorEvents.status
editorMessage.type !== NativeEvents.status
) {
return;
}
@@ -380,8 +381,8 @@ export const useEditorEvents = (
.getNoteIdForTab(editorMessage.tabId);
switch (editorMessage.type) {
case EventTypes.content:
DatabaseLogger.log("EventTypes.content");
case EditorEvents.content:
DatabaseLogger.log("EditorEvents.content");
editor.saveContent({
type: editorMessage.type,
content: editorMessage.value.html as string,
@@ -391,8 +392,8 @@ export const useEditorEvents = (
pendingChanges: editorMessage.value?.pendingChanges
});
break;
case EventTypes.title:
DatabaseLogger.log("EventTypes.title");
case EditorEvents.title:
DatabaseLogger.log("EditorEvents.title");
editor.saveContent({
type: editorMessage.type,
title: editorMessage.value?.title as string,
@@ -402,10 +403,10 @@ export const useEditorEvents = (
pendingChanges: editorMessage.value?.pendingChanges
});
break;
case EventTypes.logger:
case EditorEvents.logger:
logger.info("[EDITOR LOG]", editorMessage.value);
break;
case EventTypes.dbLogger:
case EditorEvents.dbLogger:
if (editorMessage.value.error) {
DatabaseLogger.error(
editorMessage.value.error,
@@ -418,12 +419,12 @@ export const useEditorEvents = (
DatabaseLogger.info("[EDITOR_LOG]" + editorMessage.value.message);
}
break;
case EventTypes.contentchange:
case EditorEvents.contentchange:
editor.onContentChanged(editorMessage.noteId);
break;
case EventTypes.selection:
case EditorEvents.selection:
break;
case EventTypes.reminders:
case EditorEvents.reminders:
if (!noteId) {
ToastManager.show({
heading: strings.createNoteFirst(),
@@ -441,7 +442,7 @@ export const useEditorEvents = (
onAdd: () => ReminderSheet.present(undefined, note, true)
});
break;
case EventTypes.newtag:
case EditorEvents.newtag:
if (!noteId) {
ToastManager.show({
heading: strings.createNoteFirst(),
@@ -451,7 +452,7 @@ export const useEditorEvents = (
}
ManageTagsSheet.present([noteId]);
break;
case EventTypes.tag:
case EditorEvents.tag:
if (editorMessage.value) {
if (!noteId) return;
const note = await db.notes.note(noteId);
@@ -467,7 +468,7 @@ export const useEditorEvents = (
});
}
break;
case EventTypes.filepicker:
case EditorEvents.filepicker:
editorState().isAwaitingResult = true;
const { pick } = require("./picker").default;
pick({
@@ -479,14 +480,14 @@ export const useEditorEvents = (
editorState().isAwaitingResult = false;
}, 1000);
break;
case EventTypes.download: {
case EditorEvents.download: {
const downloadAttachment =
require("../../../common/filesystem/download-attachment").default;
downloadAttachment((editorMessage.value as Attachment)?.hash, true);
break;
}
case EventTypes.getAttachmentData: {
case EditorEvents.getAttachmentData: {
const attachment = (editorMessage.value as any)
?.attachment as Attachment;
@@ -506,14 +507,14 @@ export const useEditorEvents = (
!!data,
editorMessage.resolverId
);
editor.postMessage(EditorEvents.resolve, {
editor.postMessage(NativeEvents.resolve, {
resolverId: editorMessage.resolverId,
data
});
})
.catch((e) => {
DatabaseLogger.error(e);
editor.postMessage(EditorEvents.resolve, {
editor.postMessage(NativeEvents.resolve, {
resolverId: editorMessage.resolverId,
data: undefined
});
@@ -522,26 +523,26 @@ export const useEditorEvents = (
break;
}
case EventTypes.pro:
case EditorEvents.pro:
if (editor.state.current?.isFocused) {
editor.state.current.isFocused = true;
}
eSendEvent(eOpenPremiumDialog);
break;
case EventTypes.monograph:
case EditorEvents.monograph:
publishNote();
break;
case EventTypes.properties:
case EditorEvents.properties:
showActionsheet();
break;
case EventTypes.scroll:
case EditorEvents.scroll:
editorState().scrollPosition = editorMessage.value;
break;
case EventTypes.fullscreen:
case EditorEvents.fullscreen:
editorState().isFullscreen = true;
eSendEvent(eOpenFullscreenEditor);
break;
case EventTypes.link:
case EditorEvents.link:
if (editorMessage.value.startsWith("nn://")) {
openInternalLink(editorMessage.value);
console.log(
@@ -553,7 +554,7 @@ export const useEditorEvents = (
}
break;
case EventTypes.previewAttachment: {
case EditorEvents.previewAttachment: {
const hash = (editorMessage.value as Attachment)?.hash;
const attachment = await db.attachments?.attachment(hash);
if (!attachment) return;
@@ -564,11 +565,26 @@ export const useEditorEvents = (
}
break;
}
case EventTypes.copyToClipboard: {
case EditorEvents.copyToClipboard: {
Clipboard.setString(editorMessage.value as string);
break;
}
case EventTypes.tabsChanged: {
case EditorEvents.saveScroll: {
useTabStore.getState().updateTab(editorMessage.tabId, {
session: {
...editorMessage.value
}
});
break;
}
case EditorEvents.newNote: {
eSendEvent(eOnLoadNote, {
tabId: editorMessage.tabId,
newNote: true
});
break;
}
case EditorEvents.tabsChanged: {
// useTabStore.setState({
// tabs: (editorMessage.value as any)?.tabs,
// currentTab: (editorMessage.value as any)?.currentTab
@@ -576,14 +592,14 @@ export const useEditorEvents = (
//
break;
}
case EventTypes.toc:
case EditorEvents.toc:
TableOfContents.present(editorMessage.value);
break;
case EventTypes.showTabs: {
case EditorEvents.showTabs: {
EditorTabs.present();
break;
}
case EventTypes.error: {
case EditorEvents.error: {
presentSheet({
component: (
<Issue
@@ -595,19 +611,15 @@ export const useEditorEvents = (
});
break;
}
case EventTypes.tabFocused: {
case EditorEvents.tabFocused: {
eSendEvent(eEditorTabFocused, editorMessage.tabId);
if (
(!editorMessage.value || editor.currentLoadingNoteId.current) &&
editorMessage.noteId
) {
if (!editor.state.current.initialLoadCalled) break;
if (editorMessage.noteId) {
if (!useSettingStore.getState().isAppLoading) {
const note = await db.notes.note(editorMessage.noteId);
if (note) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true,
tabId: editorMessage.tabId
});
}
@@ -619,7 +631,6 @@ export const useEditorEvents = (
if (note) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true,
tabId: editorMessage.tabId
});
}
@@ -630,7 +641,7 @@ export const useEditorEvents = (
break;
}
case EventTypes.createInternalLink: {
case EditorEvents.createInternalLink: {
LinkNote.present(
editorMessage.value.attributes,
editorMessage.resolverId as string
@@ -638,25 +649,37 @@ export const useEditorEvents = (
break;
}
case EventTypes.unlock: {
case EditorEvents.unlock: {
eSendEvent(eUnlockWithPassword, editorMessage.value);
break;
}
case EventTypes.unlockWithBiometrics: {
case EditorEvents.goBack: {
useTabStore.getState().goBack();
break;
}
case EditorEvents.goForward: {
useTabStore.getState().goForward();
break;
}
case EditorEvents.unlockWithBiometrics: {
eSendEvent(eUnlockWithBiometrics);
break;
}
case EventTypes.disableReadonlyMode: {
case EditorEvents.disableReadonlyMode: {
const noteId = editorMessage.value;
if (noteId) {
await db.notes.readonly(false, noteId);
editor.note.current[noteId] = await db.notes?.note(noteId);
useTabStore
.getState()
.updateTab(useTabStore.getState().currentTab, {
readonly: false
.updateTab(useTabStore.getState().currentTab!, {
session: {
readonly: false
}
});
setTimeout(() => {
Navigation.queueRoutesForUpdate();

View File

@@ -32,8 +32,11 @@ import {
isEncryptedContent,
isTrashItem
} from "@notesnook/core";
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { strings } from "@notesnook/intl";
import { useThemeEngineStore } from "@notesnook/theme";
import { Mutex } from "async-mutex";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import WebView from "react-native-webview";
import { DatabaseLogger, db } from "../../../common/database";
@@ -63,12 +66,10 @@ import { sleep } from "../../../utils/time";
import { unlockVault } from "../../../utils/unlock-vault";
import { onNoteCreated } from "../../notes/common";
import Commands from "./commands";
import { EventTypes } from "./editor-events";
import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types";
import { syncTabs, useTabStore } from "./use-tab-store";
import { TabSessionItem, syncTabs, useTabStore } from "./use-tab-store";
import {
EditorEvents,
clearAppState,
defaultState,
getAppState,
@@ -76,11 +77,70 @@ import {
isEditorLoaded,
post
} from "./utils";
import { Linking } from "react-native";
const loadNoteMutex = new Mutex();
type NoteWithContent = Note & {
content?: NoteContent<false>;
};
type LocalTabStateT = {
editedAt: number;
lastFocusedAt: number;
};
class LocalTabState {
state: Record<string, LocalTabStateT> = {};
noteEditedTime: Record<string, number> = {};
setEditTime(noteId: string, time: number) {
this.noteEditedTime[noteId] = time;
}
get(tabId: string) {
return this.state[tabId] || {};
}
set(tabId: string, state: Partial<LocalTabStateT>) {
this.state[tabId] = {
...this.state[tabId],
...state
};
}
clear(tabId: string) {
delete this.state[tabId];
}
reset() {
this.state = {};
}
needsRefresh(tabId: string, locked: boolean, readonly: boolean) {
const state = this.get(tabId);
const tabSession = useTabStore.getState().getTab(tabId)?.session;
const noteId = useTabStore.getState().getNoteIdForTab(tabId);
if (
tabSession?.locked !== locked ||
tabSession?.readonly !== readonly ||
!noteId
) {
console.log("tab is refreshing...");
return true;
}
console.log(
"Tab refreshing...",
state.editedAt < this.noteEditedTime[noteId]
);
console.log(state.editedAt, this.noteEditedTime[noteId]);
return !state.editedAt || state.editedAt < this.noteEditedTime[noteId];
}
}
export const useEditor = (
editorId = "",
readonly?: boolean,
@@ -99,7 +159,6 @@ export const useEditor = (
isPreview?: boolean;
};
})
| null
| undefined
>
>({});
@@ -119,15 +178,17 @@ export const useEditor = (
const lastContentChangeTime = useRef<Record<string, number>>({});
const lock = useRef(false);
const currentLoadingNoteId = useRef<string>();
const loadingState = useRef<string>();
const lastTabFocused = useRef(0);
const lastTabFocused = useRef<string>();
const localTabState = useRef<LocalTabState>(new LocalTabState());
const blockIdRef = useRef<string>();
const postMessage = useCallback(
async <T>(type: string, data: T, tabId?: number, waitFor = 300) =>
async <T>(type: string, data: T, tabId?: string, waitFor = 300) =>
await post(
editorRef,
sessionIdRef.current,
typeof tabId !== "number" ? useTabStore.getState().currentTab : tabId,
tabId || useTabStore.getState().currentTab!,
type,
data,
waitFor
@@ -142,7 +203,7 @@ export const useEditor = (
}, [commands, insets, isDefaultEditor]);
useEffect(() => {
postMessage(EditorEvents.theme, theme);
postMessage(NativeEvents.theme, theme);
}, [theme, postMessage]);
useEffect(() => {
@@ -152,14 +213,14 @@ export const useEditor = (
}, [commands, tags]);
useEffect(() => {
const event = eSubscribeEvent(eEditorTabFocused, (tabId) => {
const event = eSubscribeEvent(eEditorTabFocused, (tabId: string) => {
if (lastTabFocused.current !== tabId) lock.current = false;
lastTabFocused.current = tabId as number;
lastTabFocused.current = tabId;
});
return () => {
event?.unsubscribe();
};
});
}, []);
const overlay = useCallback(
(show: boolean, data = { type: "new" }) => {
@@ -188,11 +249,11 @@ export const useEditor = (
);
const reset = useCallback(
async (tabId: number, resetState = true, resetContent = true) => {
async (tabId: string, resetState = true, resetContent = true) => {
const noteId = useTabStore.getState().getNoteIdForTab(tabId);
if (noteId) {
currentNotes.current?.id && db.fs().cancel(noteId);
currentNotes.current[noteId] = null;
currentNotes.current[noteId] = undefined;
currentContents.current[noteId] = null;
editorSessionHistory.clearSession(noteId);
lastContentChangeTime.current[noteId] = 0;
@@ -200,18 +261,11 @@ export const useEditor = (
}
saveCount.current = 0;
loadingState.current = undefined;
currentLoadingNoteId.current = undefined;
lock.current = false;
resetContent && postMessage(EditorEvents.title, "", tabId);
resetContent && postMessage(NativeEvents.title, "", tabId);
resetContent && (await commands.clearContent(tabId));
resetContent && (await commands.clearTags(tabId));
useTabStore.getState().updateTab(tabId, {
noteId: undefined,
locked: false,
noteLocked: false,
readonly: false
});
},
[commands, editorSessionHistory, postMessage]
);
@@ -231,6 +285,16 @@ export const useEditor = (
try {
if (id && !(await db.notes?.note(id))) {
await reset(tabId);
useTabStore.getState().updateTab(tabId, {
session: {
noteId: undefined,
noteLocked: undefined,
locked: undefined,
readonly: undefined,
scrollTop: undefined,
selection: undefined
}
});
return;
}
let note = id ? await db.notes?.note(id) : undefined;
@@ -269,13 +333,6 @@ export const useEditor = (
};
}
// If note is edited, the tab becomes a persistent tab automatically.
if (useTabStore.getState().getTab(tabId)?.previewTab) {
useTabStore.getState().updateTab(tabId, {
previewTab: false
});
}
let saved = false;
setTimeout(() => {
if (saved) return;
@@ -310,7 +367,9 @@ export const useEditor = (
}
useTabStore.getState().updateTab(tabId, {
noteId: id
session: {
noteId: id
}
});
const defaultNotebook = db.settings.getDefaultNotebook();
@@ -325,7 +384,7 @@ export const useEditor = (
if (!noteData.title) {
postMessage(
EditorEvents.title,
NativeEvents.title,
currentNotes.current[id]?.title,
tabId
);
@@ -385,8 +444,8 @@ export const useEditor = (
id === useTabStore.getState().getCurrentNoteId() &&
pendingChanges
) {
postMessage(EditorEvents.title, title || note?.title, tabId);
postMessage(EditorEvents.html, data, tabId);
postMessage(NativeEvents.title, title || note?.title, tabId);
postMessage(NativeEvents.html, data, tabId);
currentNotes.current[id] = note;
}
@@ -435,179 +494,211 @@ export const useEditor = (
);
const loadNote = useCallback(
async (event: {
(event: {
item?: Note;
forced?: boolean;
newNote?: boolean;
tabId?: number;
tabId?: string;
blockId?: string;
presistTab?: boolean;
session?: TabSessionItem;
newTab?: boolean;
refresh?: boolean;
}) => {
if (!event) return;
if (event.blockId) {
blockIdRef.current = event.blockId;
}
state.current.currentlyEditing = true;
if (
!state.current.ready &&
(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab
))
) {
state.current.ready = true;
}
if (event.newNote) {
useTabStore.getState().focusEmptyTab();
const tabId = useTabStore.getState().currentTab;
currentNotes.current && (await reset(tabId));
setTimeout(() => {
if (state.current?.ready && !state.current.movedAway)
commands.focus(tabId);
});
} else {
if (!event.item) {
overlay(false);
return;
loadNoteMutex.runExclusive(async () => {
if (!event) return;
if (event.blockId) {
blockIdRef.current = event.blockId;
}
const item = event.item;
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab);
if (currentTab?.previewTab && item.id !== currentTab.noteId) {
await commands.setLoading(true, useTabStore.getState().currentTab);
}
const isLockedNote = await db.vaults.itemExists(
event.item as ItemReference
);
const tabLocked =
isLockedNote && !(event.item as NoteWithContent).content;
// If note was already opened in a tab, focus that tab.
if (typeof event.tabId !== "number") {
if (useTabStore.getState().hasTabForNote(event.item.id)) {
const tabId = useTabStore.getState().getTabForNote(event.item.id);
if (typeof tabId === "number") {
useTabStore.getState().updateTab(tabId, {
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote
});
useTabStore.getState().focusTab(tabId);
setTimeout(() => {
if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
}, 150);
}
} else {
if (event.presistTab) {
// Open note in new tab.
useTabStore.getState().newTab({
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote,
noteId: event.item.id,
previewTab: false
});
} else {
// Otherwise we focus the preview tab or create one to open the note in.
useTabStore.getState().focusPreviewTab(event.item.id, {
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote
});
}
}
} else {
if (lastTabFocused.current !== event.tabId) {
useTabStore.getState().focusTab(event.tabId);
}
}
const tabId = event.tabId || useTabStore.getState().currentTab;
if (lastTabFocused.current !== tabId) {
// if ((await waitForEvent(eEditorTabFocused, 1000)) !== tabId) {
//
// return;
// }
currentLoadingNoteId.current = item.id;
return;
}
state.current.movedAway = false;
state.current.currentlyEditing = true;
if (!tabLocked) {
await loadContent(item);
}
if (
currentNotes.current[item.id] &&
loadingState.current &&
currentContents.current[item.id]?.data &&
loadingState.current === currentContents.current[item.id]?.data
!state.current.ready &&
(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab!
))
) {
// If note is already loading, return.
return;
state.current.ready = true;
}
if (event.newNote && !currentLoadingNoteId.current) {
let tabId;
if (useTabStore.getState().tabs.length === 0) {
tabId = useTabStore.getState().newTab();
} else {
tabId = useTabStore.getState().currentTab;
await reset(tabId!, true, true);
if (
event.session?.noteId ||
useTabStore.getState().getTab(tabId!)?.session?.noteId
) {
useTabStore.getState().newTabSession(tabId!, {});
}
}
if (!state.current.ready) {
setTimeout(() => {
if (state.current?.ready && !state.current.movedAway)
commands.focus(tabId!);
});
} else {
if (!event.item) {
overlay(false);
return;
}
const item = event.item;
// If note is already open in a tab, focus that tab.
if (useTabStore.getState().hasTabForNote(item.id) && !event.newTab) {
const tabId = useTabStore.getState().getTabForNote(item.id);
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab as string);
if (
currentTab?.session?.noteId !== item.id &&
tabId !== useTabStore.getState().currentTab
) {
useTabStore.getState().focusTab(tabId as string);
return;
}
}
const isLockedNote = await db.vaults.itemExists(
event.item as ItemReference
);
const tabLocked =
isLockedNote && !(event.item as NoteWithContent).content;
const tabId = event.tabId
? event.tabId
: useTabStore.getState().currentTab;
// Check if tab needs to be refreshed.
if (!event.newTab) {
if (
!event.refresh &&
tabId &&
event.item.id === useTabStore.getState().getNoteIdForTab(tabId) &&
!localTabState.current?.needsRefresh(
tabId,
isLockedNote,
item.readonly
)
) {
commands.setLoading(false, tabId);
return;
} else {
localTabState.current?.setEditTime(
item.id,
localTabState.current?.noteEditedTime[item.id] ||
item.dateEdited
);
localTabState.current?.set(tabId!, {
editedAt:
localTabState.current?.noteEditedTime[item.id] ||
item.dateEdited
});
}
}
currentLoadingNoteId.current = item.id;
// Show loading overlay if note is not already loaded.
if (
tabId &&
(event.item?.id !== useTabStore.getState().getNoteIdForTab(tabId) ||
!currentContents.current[event.item.id]?.data)
) {
await commands.setLoading(true, tabId);
}
const session: Partial<TabSessionItem> = event.session || {
noteId: event.item.id
};
session.noteLocked = isLockedNote;
session.locked = tabLocked;
session.readonly = item.readonly;
const tab = useTabStore.getState().getTab(tabId!);
if (useTabStore.getState().tabs.length === 0 || event.newTab) {
useTabStore.getState().newTab({
session: session
});
} else {
// A new session is created if the note is changed.
// If the note is already opened, the session is updated.
if (
!tab?.session ||
(event.item.id !== tab?.session?.noteId && tab?.session?.noteId)
) {
useTabStore.getState().newTabSession(tabId!, session);
} else {
useTabStore.getState().updateTab(tabId!, {
session: session
});
}
}
if (lastTabFocused.current !== tabId) return;
if (tabBarRef.current?.page() === 2) {
state.current.movedAway = false;
}
state.current.currentlyEditing = true;
if (!tabLocked) {
await loadContent(item);
} else {
commands.focus(tabId!);
}
lastContentChangeTime.current[item.id] = item.dateEdited;
currentNotes.current[item.id] = item;
return;
if (!currentNotes.current[item.id]) return;
editorSessionHistory.newSession(item.id);
await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"),
"Saved",
tabId!
);
await postMessage(NativeEvents.title, item.title, tabId);
overlay(false);
await postMessage(
NativeEvents.html,
{
data: currentContents.current[item.id]?.data || "",
scrollTop: tab?.session?.scrollTop,
selection: tab?.session?.selection
},
tabId,
10000
);
setTimeout(() => {
if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
}, 300);
await commands.setTags(item);
commands.setSettings();
setTimeout(() => {
if (currentLoadingNoteId.current === event.item?.id) {
currentLoadingNoteId.current = undefined;
}
}, 300);
}
lastContentChangeTime.current[item.id] = 0;
currentLoadingNoteId.current = item.id;
currentNotes.current[item.id] = item;
if (!currentNotes.current[item.id]) return;
editorSessionHistory.newSession(item.id);
await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"),
strings.saved(),
tabId
);
await postMessage(EditorEvents.title, item.title, tabId);
overlay(false);
loadingState.current = currentContents.current[item.id]?.data;
await postMessage(
EditorEvents.html,
currentContents.current[item.id]?.data || "",
tabId,
10000
);
setTimeout(() => {
if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
}, 300);
loadingState.current = undefined;
await commands.setTags(item);
commands.setSettings();
setTimeout(() => {
if (currentLoadingNoteId.current === event.item?.id) {
currentLoadingNoteId.current = undefined;
}
}, 300);
}
postMessage(EditorEvents.theme, theme);
postMessage(NativeEvents.theme, theme);
});
},
[
commands,
@@ -615,7 +706,6 @@ export const useEditor = (
loadContent,
overlay,
postMessage,
readonly,
reset,
theme
]
@@ -627,96 +717,116 @@ export const useEditor = (
isLocal?: boolean
) => {
try {
await (async () => {
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) {
await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId);
}
return;
if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) {
await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId);
}
return;
}
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
if (!useTabStore.getState().hasTabForNote(noteId)) return;
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
const note = data.type === "note" ? data : await db.notes?.note(noteId);
const tab = useTabStore.getState().getTab(tabId);
lock.current = true;
const note =
data.type === "note" ? data : await db.notes?.note(noteId);
// Handle this case where note was locked on another device and synced.
let locked = note
? await db.vaults.itemExists(note as ItemReference)
: false;
lock.current = true;
if (data.type === "tiptap") {
locked = data.locked;
}
// Handle this case where note was locked on another device and synced.
const locked = note
? await db.vaults.itemExists(note as ItemReference)
: false;
useTabStore.getState().forEachNoteTab(noteId, async (tab) => {
const tabId = tab.id;
let didUnlock = false;
if (note) {
if (!locked && tab?.noteLocked) {
// Note lock removed.
if (tab.locked) {
if (useTabStore.getState().currentTab === tabId) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
if (!locked && tab?.session?.noteLocked) {
if (tab?.session?.noteLocked || tab?.session?.locked) {
if (useTabStore.getState().currentTab !== tabId) {
localTabState.current?.set(tabId, {
editedAt: 0
});
} else {
useTabStore.getState().updateTab(tabId, {
}
didUnlock = true;
useTabStore.getState().updateTab(tabId, {
session: {
locked: false,
noteLocked: false
});
commands.setLoading(true, tabId);
}
}
});
}
} else if (!tab?.noteLocked && locked) {
// Note lock added.
} else if (!tab?.session?.noteLocked && locked) {
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
session: {
locked: true,
noteLocked: true
}
});
commands.clearContent(tabId);
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
localTabState.current?.set(tabId, {
editedAt: 0
});
}
if (currentNotes.current[noteId]?.title !== note.title) {
postMessage(EditorEvents.title, note.title, tabId);
if (
currentNotes.current[noteId]?.title !== note.title ||
didUnlock
) {
postMessage(NativeEvents.title, note.title, tabId);
}
commands.setTags(note);
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
if (
currentNotes.current[noteId]?.dateEdited !== note.dateEdited ||
didUnlock
) {
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
strings.saved(),
tabId as number
tabId as string
);
}
useTabStore.getState().updateTab(tabId, {
readonly: note.readonly
});
if (tab.session?.readonly !== note.readonly || didUnlock) {
useTabStore.getState().updateTab(tabId, {
session: {
readonly: note.readonly
}
});
}
}
if (data.type === "tiptap" && note && !isLocal) {
if (lastContentChangeTime.current[noteId] >= data.dateModified) {
if (
lastContentChangeTime.current[noteId] >= data.dateModified &&
!didUnlock
) {
return;
}
if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent(data);
if (!decryptedContent) {
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
useTabStore.getState().updateTab(tab.id, {
session: {
locked: true,
noteLocked: true
}
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
@@ -724,26 +834,43 @@ export const useEditor = (
}
} else {
await postMessage(
EditorEvents.updatehtml,
decryptedContent.data,
NativeEvents.updatehtml,
{
data: decryptedContent.data,
selection: tab.session?.selection,
scrollTop: tab.session?.scrollTop
},
tabId
);
currentContents.current[note.id] = decryptedContent;
}
} else {
const _nextContent = data.data;
if (_nextContent === currentContents.current[note.id]?.data) {
if (
_nextContent === currentContents.current[note.id]?.data &&
!didUnlock
) {
return;
}
lastContentChangeTime.current[note.id] = note.dateEdited;
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
console.log(tab.session?.selection);
await postMessage(
NativeEvents.updatehtml,
{
data: _nextContent,
selection: tab.session?.selection,
scrollTop: tab.session?.scrollTop
},
tabId
);
if (!isEncryptedContent(data)) {
currentContents.current[note.id] =
data as UnencryptedContentItem;
}
}
}
})();
});
} catch (e) {
DatabaseLogger.error(e as Error, "Error when applying sync changes");
} finally {
@@ -779,7 +906,7 @@ export const useEditor = (
content?: string;
type: string;
ignoreEdit: boolean;
tabId: number;
tabId: string;
pendingChanges?: boolean;
}) => {
DatabaseLogger.log(
@@ -808,9 +935,13 @@ export const useEditor = (
if (noteId) {
lastContentChangeTime.current[noteId] = Date.now();
localTabState.current?.setEditTime(noteId, Date.now());
localTabState?.current?.set(tabId, {
editedAt: Date.now()
});
}
if (type === EventTypes.content && noteId) {
if (type === EditorEvents.content && noteId) {
currentContents.current[noteId as string] = {
data: content,
type: "tiptap",
@@ -860,7 +991,9 @@ export const useEditor = (
if (!appState) return;
state.current.isRestoringState = true;
state.current.currentlyEditing = true;
state.current.movedAway = false;
if (tabBarRef.current?.page() === 2) {
state.current.movedAway = false;
}
if (!state.current.editorStateRestored) {
state.current.isRestoringState = true;
@@ -891,9 +1024,12 @@ export const useEditor = (
!(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab
useTabStore.getState().currentTab!
))
) {
localTabState.current?.set(useTabStore.getState().currentTab!, {
editedAt: 0
});
eSendEvent(eEditorReset, "onReady");
return false;
} else {
@@ -905,41 +1041,48 @@ export const useEditor = (
const onLoad = useCallback(async () => {
setTimeout(() => {
postMessage(EditorEvents.theme, theme);
postMessage(NativeEvents.theme, theme);
});
commands.setInsets(
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
);
await commands.setSettings();
localTabState.current?.set(useTabStore.getState().currentTab!, {
editedAt: 0
});
if (!state.current.ready) {
state.current.ready = true;
}
const appState = getAppState();
if (appState?.noteId) {
const note = await db.notes?.note(appState.noteId);
if (note) {
loadNote({
item: note
});
}
} else {
const noteId = useTabStore.getState().getCurrentNoteId();
if (!noteId) {
loadNote({ newNote: true });
if (tabBarRef.current?.page() === 1) {
state.current.currentlyEditing = false;
}
} else if (state.current?.initialLoadCalled) {
const note = currentNotes.current[noteId];
if (!state.current?.initialLoadCalled) {
const url = await Linking.getInitialURL();
let noteId = url && new URL(url).searchParams.get("id");
if (noteId) {
const note = await db.notes?.note(noteId);
if (note) {
loadNote({
item: note
});
}
}
if (!state.current?.initialLoadCalled) {
tabBarRef.current?.goToPage(1);
} else {
noteId = useTabStore.getState().getCurrentNoteId() || null;
if (!noteId) {
loadNote({ newNote: true });
if (tabBarRef.current?.page() === 1) {
state.current.currentlyEditing = false;
}
} else {
const note = await db.notes.note(noteId);
if (note) {
loadNote({
item: note
});
}
}
state.current.initialLoadCalled = true;
}
overlay(false);

View File

@@ -16,18 +16,27 @@ 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 {
TabHistory as TabHistoryType,
TabSessionHistory
} from "@notesnook/common";
import { getId } from "@notesnook/core";
import { MMKVLoader } from "react-native-mmkv-storage";
import create from "zustand";
import { persist, StateStorage } from "zustand/middleware";
import { db } from "../../../common/database";
import { MMKV } from "../../../common/database/mmkv";
import { eSendEvent } from "../../../services/event-manager";
import { eOnLoadNote } from "../../../utils/events";
import { editorController } from "./utils";
class History {
history: number[];
class TabHistory {
history: string[];
constructor() {
this.history = [0];
this.history = [];
}
add(item: number) {
add(item: string) {
const index = this.history.findIndex((id) => item === id);
if (index !== -1) {
// Item already exists, move it to the top
@@ -36,19 +45,19 @@ class History {
this.history.unshift(item); // Add item to the beginning of the array
useTabStore.setState({
tabHistory: this.history.slice()
historyNew: this.history.slice()
});
return true; // Item added successfully
}
remove(id: number) {
remove(id: string) {
const index = this.history.findIndex((item) => item === id);
if (index >= -1 && index < this.history.length) {
const removedItem = this.history.splice(index, 1)[0];
return removedItem;
}
useTabStore.setState({
tabHistory: this.history.slice()
historyNew: this.history.slice()
});
return null; // Invalid index
}
@@ -59,7 +68,7 @@ class History {
return restoredItem;
}
useTabStore.setState({
tabHistory: this.history.slice()
historyNew: this.history.slice()
});
return null; // History is empty
}
@@ -69,147 +78,359 @@ class History {
}
}
export type TabItem = {
id: number;
export type TabSessionItem = {
id: string;
noteId?: string;
previewTab?: boolean;
readonly?: boolean;
locked?: boolean;
scrollTop?: number;
selection?: { to: number; from: number };
noteLocked?: boolean;
pinned?: boolean;
locked?: boolean;
readonly?: boolean;
};
const history = new History();
const TabSessionStorageKV = new MMKVLoader()
.withInstanceID("tab-session-storage")
.disableIndexing()
.initialize();
class TabSessionStorage {
static storage: typeof TabSessionStorageKV = TabSessionStorageKV;
static get(id: string): TabSessionItem | null {
return TabSessionStorage.storage.getMap(id);
}
static set(id: string, session: TabSessionItem): void {
TabSessionStorage.storage.setMap(id, session);
}
static update(id: string, session: Partial<TabSessionItem>) {
if (!id) return;
const currentSession = TabSessionStorage.get(id);
const newSession = {
...currentSession,
...session,
id: currentSession?.id || session.id || id
};
TabSessionStorage.set(id, newSession as TabSessionItem);
return newSession;
}
static remove(id: string) {
TabSessionStorageKV.removeItem(id);
}
}
export function syncTabs(
type: "tabs" | "history" | "biometry" | "all" = "all"
) {
const data: Partial<TabStore> = {};
if (type === "tabs" || type === "all") {
data.tabs = useTabStore.getState().tabs;
data.currentTab = useTabStore.getState().currentTab;
}
if (type === "history" || type === "all") {
data.canGoBack = useTabStore.getState().canGoBack;
data.canGoForward = useTabStore.getState().canGoForward;
data.sessionId = useTabStore.getState().sessionId;
}
if (type === "biometry" || type === "all") {
data.biometryAvailable = useTabStore.getState().biometryAvailable;
data.biometryEnrolled = useTabStore.getState().biometryEnrolled;
}
editorController.current?.commands.doAsync(`
globalThis.tabStore?.setState(${JSON.stringify(data)});
`);
}
export const tabSessionHistory = new TabSessionHistory({
get() {
return useTabStore.getState();
},
set(state) {
useTabStore.setState({
...state
});
}
});
export type TabItem = {
id: string;
pinned?: boolean;
needsRefresh?: boolean;
session?: Partial<TabSessionItem>;
};
const history = new TabHistory();
export type TabStore = {
tabs: TabItem[];
currentTab: number;
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => void;
currentTab: string;
updateTab: (id: string, options: Omit<Partial<TabItem>, "id">) => void;
focusPreviewTab: (
noteId: string,
options: Omit<Partial<TabItem>, "id">
) => void;
removeTab: (index: number) => void;
removeTab: (index: string) => void;
moveTab: (index: number, toIndex: number) => void;
newTab: (options?: Omit<Partial<TabItem>, "id">) => void;
focusTab: (id: number) => void;
getNoteIdForTab: (id: number) => string | undefined;
getTabForNote: (noteId: string) => number | undefined;
newTab: (options?: Omit<Partial<TabItem>, "id">) => string;
focusTab: (id: string) => void;
getNoteIdForTab: (id: string) => string | undefined;
getTabForNote: (noteId: string) => string | undefined;
getTabsForNote: (noteId: string) => TabItem[];
forEachNoteTab: (noteId: string, cb: (tab: TabItem) => void) => void;
hasTabForNote: (noteId: string) => boolean;
focusEmptyTab: () => void;
getCurrentNoteId: () => string | undefined;
getTab: (tabId: number) => TabItem | undefined;
tabHistory: number[];
getTab: (tabId: string) => TabItem | undefined;
newTabSession: (
id: string,
options?: Omit<Partial<TabSessionItem>, "id">
) => void;
historyNew: string[];
biometryAvailable?: boolean;
biometryEnrolled?: boolean;
tabSessionHistory: TabHistoryType;
goBack(): void;
goForward(): void;
loadSession: (id: string) => Promise<boolean>;
canGoBack?: boolean;
canGoForward?: boolean;
sessionId?: string;
};
function getId(id: number, tabs: TabItem[]): number {
const exists = tabs.find((t) => t.id === id);
if (exists) {
return getId(id + 1, tabs);
}
return id;
}
const DEFAULT_TABS = {
tabs: [
{
id: "679da59a3924d4bd56d16d3f",
session: {
id: "679da5a5667a16db2353a062"
}
}
],
tabSessionHistory: {
"679da59a3924d4bd56d16d3f": {
backStack: ["679da5a5667a16db2353a062"],
forwardStack: [] as string[]
}
} as TabHistoryType,
historyNew: ["679da59a3924d4bd56d16d3f"],
currentTab: "679da59a3924d4bd56d16d3f"
} as TabStore;
export function syncTabs() {
editorController.current?.commands.doAsync(`
globalThis.tabStore?.setState({
tabs: ${JSON.stringify(useTabStore.getState().tabs)},
currentTab: ${useTabStore.getState().currentTab},
biometryAvailable: ${useTabStore.getState().biometryAvailable},
biometryEnrolled: ${useTabStore.getState().biometryEnrolled}
});
`);
export function resetTabStore() {
useTabStore.setState({
...DEFAULT_TABS
});
TabSessionStorage.storage.clearStore();
}
export const useTabStore = create<TabStore>(
persist(
(set, get) => ({
tabs: [
{
id: 0
...DEFAULT_TABS,
newTabSession: (
_id?: string,
options?: Omit<Partial<TabSessionItem>, "id">
) => {
const tabId = _id || (get().currentTab as string);
const sessionHistory = get().tabSessionHistory[tabId];
let oldSessionId: string | undefined = undefined;
if (sessionHistory) {
const allSessions = sessionHistory.backStack.concat(
sessionHistory.forwardStack
);
allSessions.forEach((id) => {
if (TabSessionStorage.get(id)?.noteId === options?.noteId) {
oldSessionId = id;
}
});
}
],
tabHistory: [0],
history: new History(),
currentTab: 0,
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => {
if (!options) return;
const index = get().tabs.findIndex((t) => t.id === id);
const sessionId =
oldSessionId &&
tabSessionHistory.currentSessionId(tabId) === oldSessionId
? oldSessionId
: tabSessionHistory.add(tabId, oldSessionId);
let session: Partial<TabSessionItem>;
if (!oldSessionId) {
session = {
id: sessionId,
...options
};
TabSessionStorage.set(sessionId, session as TabSessionItem);
} else {
session = {
id: sessionId,
...TabSessionStorage.get(oldSessionId),
...options
};
}
const index = get().tabs.findIndex((t) => t.id === tabId);
if (index == -1) return;
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index],
...options
};
session: session
} as TabItem;
set({
tabs: tabs
});
syncTabs();
},
focusPreviewTab: (
noteId: string,
options: Omit<Partial<TabItem>, "id" | "noteId">
) => {
const index = get().tabs.findIndex((t) => t.previewTab);
if (index === -1)
return get().newTab({
noteId,
previewTab: true,
...options
});
updateTab: (id: string, options: Omit<Partial<TabItem>, "id">) => {
if (!options) return;
const index = get().tabs.findIndex((t) => t.id === id);
if (index == -1) return;
const tabs = [...get().tabs];
const sessionId =
options.session?.id || (tabs[index].session?.id as string);
const updatedSession = !options.session
? tabs[index].session
: TabSessionStorage.update(sessionId, options.session);
tabs[index] = {
...tabs[index],
...options,
previewTab: true,
noteId: noteId
};
session: updatedSession
} as TabItem;
set({
tabs: tabs
});
get().focusTab(tabs[index].id);
syncTabs();
},
removeTab: (id: number) => {
const index = get().tabs.findIndex((t) => t.id === id);
goBack: async () => {
const currentTab = get().currentTab;
if (!currentTab) return;
if (!tabSessionHistory.canGoBack(currentTab)) return;
const id = tabSessionHistory.back(currentTab) as string;
const sessionLoaded = await get().loadSession(id);
if (!sessionLoaded) {
tabSessionHistory.remove(currentTab, id);
TabSessionStorage.remove(id);
if (!tabSessionHistory.canGoBack(currentTab)) {
tabSessionHistory.forward(currentTab);
syncTabs();
} else {
return get().goBack();
}
} else {
syncTabs();
}
},
goForward: async () => {
const currentTab = get().currentTab;
if (!currentTab) return;
if (!tabSessionHistory.canGoForward(currentTab)) return;
const id = tabSessionHistory.forward(currentTab) as string;
if (!(await get().loadSession(id))) {
tabSessionHistory.remove(currentTab, id);
TabSessionStorage.remove(id);
if (!tabSessionHistory.canGoForward(currentTab)) {
tabSessionHistory.back(currentTab);
syncTabs();
} else {
return get().goForward();
}
} else {
syncTabs();
}
},
loadSession: async (id: string) => {
const session = TabSessionStorage.get(id);
if (!session) return false;
const note = session?.noteId
? await db.notes.note(session?.noteId)
: undefined;
if (note) {
const isLocked = await db.vaults.itemExists(note);
session.noteLocked = isLocked;
session.locked = isLocked;
session.readonly = note.readonly;
} else if (session.noteId) {
console.log("Failed to load session...");
return false;
}
eSendEvent(eOnLoadNote, {
item: note,
newNote: !note,
tabId: get().currentTab,
session: session
});
return true;
},
focusPreviewTab: (
noteId: string,
options: Omit<Partial<TabItem>, "id" | "noteId">
) => {},
removeTab: (id: string) => {
const index = get().tabs.findIndex((t) => t.id === id);
if (index > -1) {
const isFocused = id === get().currentTab;
const nextTabs = get().tabs.slice();
nextTabs.splice(index, 1);
history.remove(id);
const tabSessions = tabSessionHistory.getTabHistory(id);
tabSessions.back.forEach((id) => TabSessionStorage.remove(id));
tabSessions.forward.forEach((id) => TabSessionStorage.remove(id));
tabSessionHistory.clearStackForTab(id);
if (nextTabs.length === 0) {
nextTabs.push({
id: 0
const id = getId();
set({
tabs: [{ id: id }]
});
get().newTabSession(id, {});
get().focusTab(id);
} else {
set({
tabs: nextTabs
});
if (isFocused) {
const lastTab = history.restoreLast();
if (lastTab) get().focusTab(lastTab);
}
}
set({
tabs: nextTabs
});
get().focusTab(
isFocused ? history.restoreLast() || 0 : get().currentTab
);
syncTabs();
}
},
newTab: (options) => {
const id = getId(get().tabs.length, get().tabs);
const nextTabs = [
...get().tabs,
{
id: id,
...options
}
];
const id = getId();
set({
tabs: nextTabs
tabs: [
...get().tabs,
{
id: id,
...options
}
]
});
get().newTabSession(id, options?.session || {});
get().focusTab(id);
return id;
},
focusEmptyTab: () => {
const index = get().tabs.findIndex((t) => !t.noteId);
const index = get().tabs.findIndex((t) => !t.session?.noteId);
if (index === -1) return get().newTab();
get().focusTab(get().tabs[index].id);
@@ -223,37 +444,46 @@ export const useTabStore = create<TabStore>(
syncTabs();
},
focusTab: (id: number) => {
focusTab: (id: string) => {
history.add(id);
set({
currentTab: id
currentTab: id,
canGoBack: tabSessionHistory.canGoBack(id),
canGoForward: tabSessionHistory.canGoForward(id),
sessionId: tabSessionHistory.currentSessionId(id)
});
syncTabs();
},
getNoteIdForTab: (id: number) => {
return get().tabs.find((t) => t.id === id)?.noteId;
getNoteIdForTab: (id: string) => {
return get().tabs.find((t) => t.id === id)?.session?.noteId;
},
hasTabForNote: (noteId: string) => {
return (
typeof get().tabs.find((t) => t.noteId === noteId)?.id === "number"
);
return !!get().tabs.find((t) => t.session?.noteId === noteId);
},
getTabForNote: (noteId: string) => {
return get().tabs.find((t) => t.noteId === noteId)?.id;
return get().tabs.find((t) => t.session?.noteId === noteId)?.id;
},
getTabsForNote(noteId: string) {
return get().tabs.filter((t) => t.session?.noteId === noteId);
},
forEachNoteTab: (noteId: string, cb: (tab: TabItem) => void) => {
const tabs = get().tabs.filter((t) => t.session?.noteId === noteId);
tabs.forEach(cb);
},
getCurrentNoteId: () => {
return get().tabs.find((t) => t.id === get().currentTab)?.noteId;
return get().tabs.find((t) => t.id === get().currentTab)?.session
?.noteId;
},
getTab: (tabId) => {
return get().tabs.find((t) => t.id === tabId);
}
}),
{
name: "tabs-storage",
name: "tabs-storage-v5",
getStorage: () => MMKV as unknown as StateStorage,
onRehydrateStorage: () => {
return (state) => {
history.history = state?.tabHistory.slice() || [];
history.history = state?.historyNew || [];
};
}
}

View File

@@ -31,6 +31,8 @@ import { eOnLoadNote } from "../../../utils/events";
import { NotesnookModule } from "../../../utils/notesnook-module";
import { AppState, EditorState, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
export const textInput = createRef<TextInput>();
export const editorController =
createRef<useEditorType>() as MutableRefObject<useEditorType>;
@@ -46,19 +48,6 @@ export function editorState() {
return editorController.current?.state.current || defaultState;
}
export const EditorEvents = {
html: "native:html",
updatehtml: "native:updatehtml",
title: "native:title",
theme: "native:theme",
titleplaceholder: "native:titleplaceholder",
logger: "native:logger",
status: "native:status",
keyboardShown: "native:keyboardShown",
attachmentData: "native:attachment-data",
resolve: "native:resolve"
};
export function randId(prefix: string) {
return Math.random()
.toString(36)
@@ -72,15 +61,15 @@ export function makeSessionId(id?: string) {
export async function isEditorLoaded(
ref: RefObject<WebView>,
sessionId: string,
tabId: number
tabId: string
) {
return await post(ref, sessionId, tabId, EditorEvents.status);
return await post(ref, sessionId, tabId, NativeEvents.status);
}
export async function post<T>(
ref: RefObject<WebView>,
sessionId: string,
tabId: number,
tabId: string,
type: string,
value: T | null = null,
waitFor = 300
@@ -185,7 +174,7 @@ export async function openInternalLink(url: string) {
if (!data?.id) return false;
if (
data.id ===
useTabStore.getState().getNoteIdForTab(useTabStore.getState().currentTab)
useTabStore.getState().getNoteIdForTab(useTabStore.getState().currentTab!)
) {
if (data.params?.blockId) {
setTimeout(() => {

View File

@@ -148,7 +148,7 @@ const restoreBackup = async (options: {
let count = 0;
await db.transaction(async () => {
let passwordOrKey: PasswordOrKey;
let passwordOrKey: PasswordOrKey | undefined = undefined;
for (const path of extractedBackupFiles) {
if (path === ".nnbackup" || path === "attachments") continue;
@@ -166,7 +166,7 @@ const restoreBackup = async (options: {
passwordOrKey = !isEncryptedBackup
? ({} as PasswordOrKey)
: await withPassword();
: passwordOrKey || (await withPassword());
if (
isEncryptedBackup &&

View File

@@ -23,7 +23,7 @@ import {
StackActions,
useNavigation
} from "@react-navigation/native";
import React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, TextInput, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import ToggleSwitch from "toggle-switch-react-native";
@@ -103,6 +103,11 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
}
};
useEffect(() => {
setIsHidden(item.hidden && item.hidden(item.property || current));
setIsDisabled(item.disabled && item.disabled(item.property || current));
}, [current, item]);
return isHidden ? null : (
<Pressable
disabled={item.type === "component" || isDisabled}

View File

@@ -451,7 +451,7 @@ export const settingsGroups: SettingSection[] = [
"local",
"partial"
);
if (result.error) throw result.error as Error;
if (result?.error) throw result.error as Error;
} catch (e) {
DatabaseLogger.error(e);
const error = e;

View File

@@ -30,7 +30,11 @@ import { DatabaseLogger, db } from "../common/database";
import filesystem, { FileStorage } from "../common/filesystem";
import { cacheDir, copyFileAsync } from "../common/filesystem/utils";
import { presentDialog } from "../components/dialog/functions";
import { endProgress, updateProgress } from "../components/dialogs/progress";
import {
endProgress,
startProgress,
updateProgress
} from "../components/dialogs/progress";
import { eCloseSheet } from "../utils/events";
import { sleep } from "../utils/time";
import { ToastManager, eSendEvent, presentSheet } from "./event-manager";
@@ -155,6 +159,7 @@ async function updateNextBackupTime(type: "full" | "partial") {
[type === "full" ? "lastFullBackupDate" : "lastBackupDate"]: Date.now()
});
}
let backupRunning = false;
/**
* @param {boolean=} progress
* @param {string=} context
@@ -164,7 +169,23 @@ async function run(
progress = false,
context?: string,
backupType: "full" | "partial" = "partial"
) {
): Promise<{
path?: string;
error?: Error;
report?: boolean;
}> {
if (backupRunning) {
if (progress) {
startProgress({
title: strings.backingUpData(backupType),
paragraph: strings.backupDataDesc(),
progress: "Backup in progress...",
canHideProgress: true
});
}
return {};
}
backupRunning = true;
const androidBackupDirectory = (await checkBackupDirExists(
false,
context
@@ -192,10 +213,11 @@ async function run(
);
if (progress) {
presentSheet({
startProgress({
title: strings.backingUpData(backupType),
paragraph: strings.backupDataDesc(),
progress: true
progress: "Preparing backup...",
canHideProgress: true
});
}
@@ -217,6 +239,7 @@ async function run(
try {
const user = await db.user.getUser();
DatabaseLogger.info(`Backup started: ${backupType}`);
for await (const file of db.backup.export({
type: "mobile",
encrypt: SettingsService.get().encryptedBackup && !!user,
@@ -244,12 +267,16 @@ async function run(
}
}
DatabaseLogger.info(`Backup complete: ${backupType}. Creating zip file...`);
updateProgress({
progress: "Creating backup zip file..."
});
await zip(zipSourceFolder, zipOutputFile);
DatabaseLogger.info(`Backup zip file created: ${zipOutputFile}`);
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
@@ -258,21 +285,17 @@ async function run(
"application/nnbackupz"
);
DatabaseLogger.info("Copying backup file to user selected directory...");
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
DatabaseLogger.info("Backup saved to user selected directory.");
path = file.uri;
} else {
path = zipOutputFile;
}
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
/* empty */
});
}
cleanupAfterBackup(zipSourceFolder, zipOutputFile);
updateNextBackupTime(backupType || "partial");
@@ -283,6 +306,8 @@ async function run(
const canShowCompletionStatus =
progress && SettingsService.get().showBackupCompleteSheet;
backupRunning = false;
if (context) {
return {
path: path
@@ -300,11 +325,11 @@ async function run(
type: "success",
context: "global"
});
return {
path: path
};
} catch (e) {
backupRunning = false;
ToastManager.error(e as Error, strings.backupFailed(), context || "global");
if (
@@ -315,27 +340,30 @@ async function run(
return run(progress, context, backupType);
}
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
/* empty */
});
}
cleanupAfterBackup(zipSourceFolder, zipOutputFile);
DatabaseLogger.error(e);
await sleep(300);
if (progress) {
endProgress();
}
return {
error: e,
error: e as Error,
report: true
};
}
}
function cleanupAfterBackup(zipSourceFolder: string, zipOutputFile: string) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
/* empty */
});
}
}
async function checkBackupRequired(
type: "daily" | "off" | "useroff" | "weekly" | "monthly" | "never",
lastBackupDateType: "lastBackupDate" | "lastFullBackupDate" = "lastBackupDate"

View File

@@ -43,6 +43,7 @@ import { useRelationStore } from "../stores/use-relation-store";
import { useReminderStore } from "../stores/use-reminder-store";
import { useSettingStore } from "../stores/use-setting-store";
import { useUserStore } from "../stores/use-user-store";
import { eOnLoadNote } from "../utils/events";
import { tabBarRef } from "../utils/global-refs";
import { convertNoteToText } from "../utils/note-to-text";
import { NotesnookModule } from "../utils/notesnook-module";
@@ -449,19 +450,10 @@ async function loadNote(id: string, jump: boolean) {
})
);
const isLocked = await db.vaults.itemExists({
type: "note",
id: id
});
const tab = useTabStore.getState().getTabForNote(id);
if (tab !== undefined) {
useTabStore.getState().focusTab(tab);
} else {
useTabStore.getState().focusPreviewTab(id, {
noteId: id,
readonly: note.readonly,
noteLocked: isLocked
if (useTabStore.getState().currentTab !== tab) {
eSendEvent(eOnLoadNote, {
note: note
});
}
}

View File

@@ -45,7 +45,17 @@ function resetSettings() {
...defaultSettings,
introCompleted: true,
serverUrls: get().serverUrls,
backupDirectoryAndroid: undefined
backupDirectoryAndroid: get().backupDirectoryAndroid,
darkTheme: get().darkTheme,
lighTheme: get().lighTheme,
useSystemTheme: get().useSystemTheme,
colorScheme: get().colorScheme,
defaultSnoozeTime: get().defaultSnoozeTime,
defaultFontFamily: get().defaultFontFamily,
defaultFontSize: get().defaultFontSize,
privacyScreen: get().privacyScreen,
corsProxy: get().corsProxy,
showBackupCompleteSheet: get().showBackupCompleteSheet
};
MMKV.setString("appSettings", JSON.stringify(settings));

View File

@@ -116,7 +116,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3037
versionCode 3040
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -62,4 +62,5 @@
#Gson
-keepattributes Signature
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
-keep class * extends com.google.gson.reflect.TypeToken
-keep class com.streetwriters.notesnook.datatypes.* { *; }

View File

@@ -60,6 +60,7 @@
android:theme="@style/BootTheme"
android:largeHeap="true"
android:supportsRtl="false"
android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:supportsRtl">
<receiver android:exported="false" android:label="@string/reminders_title" android:name=".ReminderWidgetProvider">

View File

@@ -31,14 +31,20 @@ public class NotePreviewWidget extends AppWidgetProvider {
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra(OpenNoteId, note.getId());
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(RCTNNativeModule.IntentType, "OpenNote");
intent.setData(Uri.parse("https://notesnook.com/open_note"));
intent.setData(Uri.parse("https://notesnook.com/open_note?id=" + note.getId()));
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
views.setOnClickPendingIntent(R.id.open_note, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
}
private static Bundle getActivityOptionsBundle() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ActivityOptions activityOptions = ActivityOptions.makeBasic();

View File

@@ -4,6 +4,7 @@ import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
@@ -45,6 +46,7 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor
SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Gson gson = new Gson();
reminders = gson.fromJson(preferences.getString("remindersList","[]"), new TypeToken<List<Reminder>>(){}.getType());
}
@Override
@@ -64,14 +66,16 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor
boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty();
RemoteViews views = new RemoteViews(context.getPackageName(), useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout);
views.setTextViewText(R.id.reminder_title, reminder.getTitle());
if (!useMiniLayout) {
if (!useMiniLayout) {
views.setTextViewText(R.id.reminder_description, reminder.getDescription());
}
}
views.setTextViewText(R.id.reminder_time, reminder.getFormattedTime());
final Intent fillInIntent = new Intent();
final Bundle extras = new Bundle();
extras.putString(ReminderViewsService.OpenReminderId, reminder.getId());
fillInIntent.setData(Uri.parse("https://notesnook.com/open_reminder?id=" + reminder.getId()));
fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder");
fillInIntent.putExtras(extras);
views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent);
@@ -86,11 +90,12 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor
@Override
public int getViewTypeCount() {
return 1;
return 2;
}
@Override
public long getItemId(int position) {
return position;
}

View File

@@ -34,12 +34,13 @@ public class ReminderWidgetProvider extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, RemoteViews views) {
Intent listview_intent_template = new Intent(context, MainActivity.class);
listview_intent_template.setData(Uri.parse("https://notesnook.com/open_reminder"));
listview_intent_template.setAction(Intent.ACTION_VIEW);
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, getActivityOptionsBundle());
views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent);
Intent new_reminder_intent = new Intent(context, MainActivity.class);
new_reminder_intent.putExtra(NewReminder, NewReminder);
new_reminder_intent.setAction(Intent.ACTION_VIEW);
new_reminder_intent.putExtra(RCTNNativeModule.IntentType, "NewReminder");
new_reminder_intent.setData(Uri.parse("https://notesnook.com/new_reminder"));
PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());

View File

@@ -5,7 +5,7 @@
<color name="light_blue_600">#FF039BE5</color>
<color name="light_blue_900">#FF01579B</color>
<color name="bootsplash_background">#FFFFFF</color>
<color name="background">#D8000000</color>
<color name="border">#CCCCCC</color>
<color name="background">#DCEDEDED</color>
<color name="border">#BFBFBF</color>
<color name="text">#1D1D1D</color>
</resources>

View File

@@ -1,6 +1,6 @@
- Added a widget to add a note to home screen for quick access
- Added a widget that shows upcoming reminders on home screen
- Now you can share an image with other apps from image viewer
- New and improved editor tabs with history
- Fixed a crash on app launch faced by some users
- Fixed homescreen note widget does not open correct note
- Many small bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -1063,7 +1063,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2124;
CURRENT_PROJECT_VERSION = 2127;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1137,7 +1137,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.27;
MARKETING_VERSION = 3.0.30;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1168,7 +1168,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2124;
CURRENT_PROJECT_VERSION = 2127;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1242,7 +1242,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.27;
MARKETING_VERSION = 3.0.30;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1401,7 +1401,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2124;
CURRENT_PROJECT_VERSION = 2127;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1413,7 +1413,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.27;
MARKETING_VERSION = 3.0.30;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1444,7 +1444,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2124;
CURRENT_PROJECT_VERSION = 2127;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1457,7 +1457,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.27;
MARKETING_VERSION = 3.0.30;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1487,7 +1487,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2124;
CURRENT_PROJECT_VERSION = 2127;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1561,7 +1561,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.27;
MARKETING_VERSION = 3.0.30;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1592,7 +1592,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2124;
CURRENT_PROJECT_VERSION = 2127;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1667,7 +1667,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.27;
MARKETING_VERSION = 3.0.30;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -1820,7 +1820,7 @@ SPEC CHECKSUMS:
Base64: cecfb41a004124895a7bcee567a89bae5a89d49b
BEMCheckBox: 5ba6e37ade3d3657b36caecc35c8b75c6c2b1a4e
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
callstack-repack: 2db7cbeced7cd5da360d01965e0b983f6dacfd54
callstack-repack: 721b034d0662f47388719bf26c00f838da6880d5
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
FBLazyVector: ac12dc084d1c8ec4cc4d7b3cf1b0ebda6dab85af
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
@@ -1837,103 +1837,103 @@ SPEC CHECKSUMS:
RCTTypeSafety: f5ecbc86c5c5fa163c05acb7a1c5012e15b5f994
React: fc9fa7258eff606f44d58c5b233a82dc9cf09018
React-callinvoker: e3fab14d69607fb7e8e3a57e5a415aed863d3599
React-Codegen: 49efb5917c904b5b01ef708311a2f1773f4fe73e
React-Core: 3a5fd9e781cecf87803e5b091496a606a3df774a
React-CoreModules: cbf4707dafab8f9f826ac0c63a07d0bf5d01e256
React-cxxreact: 7b188556271e3c7fdf22a04819f6a6225045b9dd
React-Codegen: ff6c425afab9288a1573337111741d30363bf962
React-Core: c3f589f104983dec3c3eeec5e70d61aa811bc236
React-CoreModules: 864932ddae3ead5af5bfb05f9bbc2cedcb958b39
React-cxxreact: bd9146108c44e6dbb99bba4568ce7af0304a2419
React-debug: d30893c49ae1bce4037ea5cd8bb2511d2a38d057
React-Fabric: 826729dd2304fda9b89ff0a579f60ba2a470bc26
React-FabricImage: 2ad1fb8ffa5778eda9ed204a7b3cdd70bc333ce7
React-Fabric: a171830e52baf8ec2b175c6a3791e01bbb92f1fb
React-FabricImage: ad154af0067f4b5dc5a41f607e48ee343641e903
React-featureflags: 4ae83e72d9a92452793601ac9ac7d2280e486089
React-graphics: 61a026e1c1e7e20d20ac9fec6f6de631732b233d
React-hermes: a7054fbcbda3957e3c5eaad06ef9bf79998d535a
React-ImageManager: 2bbd6eb2e696bc680f76f84563e4b87d241614e1
React-jserrorhandler: 56fa04d49bfbe54ddfece7916673a73ebfea286b
React-jsi: f3ce1dd2e950b6ad12b65ea3ef89168f1b94c584
React-jsiexecutor: b4df3a27973d82f9abf3c4bd0f88e042cda25f16
React-jsinspector: 97ea746c023687de7313ee289817d6991d596c7d
React-jsitracing: 3b6060bbf5317663667e1dd93560c7943ab86ccc
React-logger: 257858bd55f3a4e1bc0cf07ddc8fb9faba6f8c7c
React-Mapbuffer: 6c1cacdbf40b531f549eba249e531a7d0bfd8e7f
react-native-actions-shortcuts: 5d9cf0c9c308333dfcc1e05c3f9afa8c428e2533
react-native-background-actions: 2c251c986f23347f9c1722f05fd296938f60edb1
react-native-begin-background-task: 3b889e07458afc5822a7277cf9cbc7cd077e39ee
react-native-blob-util: 2d36383bb52c15c5451be81cb7ddf22bc34a12a6
react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8
react-native-date-picker: 93e43b3084cea595b4d68b1405d6d99849663bd6
react-native-document-picker: ec07866a30707f23660c0f3ae591d669d3e89096
react-native-fingerprint-scanner: be63e626b31fb951780a5fac5328b065a61a3d6e
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-gzip: c5e87ee9e359f02350e3a2ee52eb35eddc398868
react-native-html-to-pdf-lite: 21bfb169bf4cbcd7bec9f736975ee1b3f5292d4a
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4
react-native-keep-awake: caee3ff89eaa21dfe29010f0d143566874a04441
react-native-mmkv-storage: 3892853c11ab8a0c3a3f9ef3b0657aa9a5f616c9
react-native-netinfo: ccbe1085dffd16592791d550189772e13bf479e2
react-native-notification-sounds: da78c828fe1bcbb92d8b505d5261890ed315ff39
react-native-orientation: f1caf84d65f1a4fd4511a18f2b924e634ad7a628
react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
react-native-quick-sqlite: 18e1367c34faac90e37f6eb3e78c196e9b674b5d
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
react-native-screenguard: 8b36a3df84c76cd2b82c477f71c26fa1c8cc14a0
react-native-share-extension: 01b25fc15faf2510cc2529e98ca444f64e46238d
react-native-sodium: 4cb76086943a7f60c42b40ebca866695b360a196
react-native-theme-switch-animation: d3eb50365a3829ce5572628888fa514752703f61
react-native-webview: 553abd09f58e340fdc7746c9e2ae096839e99911
React-graphics: ed7d57965140168de86835946e8f1210c72c65dc
React-hermes: 177b1efdf3b8f10f4ca12b624b83fb4d4ccb2884
React-ImageManager: 3a50d0ee0bf81b1a6f23a0c5b30388293bcd6004
React-jserrorhandler: dcd62f5ca1c724c19637595ef7f45b78018e758f
React-jsi: 0abe1b0881b67caf8d8df6a57778dd0d3bb9d9a5
React-jsiexecutor: f6ca8c04f19f6a3acaa9610f7fb728f39d6e3248
React-jsinspector: db98771eae84e6f86f0ca5d9dcc572baadbfefc0
React-jsitracing: f8367edacc50bb3f9f056a5aeafb8cee5849fafb
React-logger: 780b9ee9cec7d44eabc4093de90107c379078cb6
React-Mapbuffer: f544f00b98dbdd8cbae96dd2bdb8b47f719976e0
react-native-actions-shortcuts: 65fb9cb4a58c71af7382663c1822dda061dbf1b0
react-native-background-actions: 40e09df6ea8c7d2753f6c1d75f6f1eee4a9bc35f
react-native-begin-background-task: 2191f2a84b0328932a3d44db4361e666fee781bb
react-native-blob-util: 4be49d669870f01645b63d33342718e95d0d48b1
react-native-config: 136f9755ccc991cc6438053a44363259ad4c7813
react-native-date-picker: bbf77c50dce9da2beb11e2f67f3300195ba27d81
react-native-document-picker: f4ccd6f6f369da1409e609788181eac66cc1a2d8
react-native-fingerprint-scanner: 562585260768cad51ae48e4b505d28c8731aecfa
react-native-get-random-values: 419569b6ed3d15bfb9b6781b2f2e058f8e8d2698
react-native-gzip: 8d602277c2564591f04dd1cec4043acc8350dcc3
react-native-html-to-pdf-lite: d606d3b72d9696c7a6d1e5f5b7536783faacef14
react-native-image-resizer: ff3c9ab0ce235c4c00f49d0a373043cb4b8781be
react-native-in-app-review: b3d1eed3d1596ebf6539804778272c4c65e4a400
react-native-keep-awake: bda639e941ce138506c18f1b999dcbf16d4c77b8
react-native-mmkv-storage: 8dddc9d16248bc9a4d9566ac5c97c318692d299c
react-native-netinfo: 66c9ac0d0ae92a57a9ed99ab8a75429740700c73
react-native-notification-sounds: ce106d58df0dd384bccbd2e84fb53accab7cc068
react-native-orientation: db2655b95b4123f17e75dcb04ee8b8b1294ce7ee
react-native-pdf: da10473ae54555c373c62af671da8fe0eef846e2
react-native-quick-sqlite: 80b5b3405bfc92e97b6041d2c2a446800bea6ad7
react-native-safe-area-context: b72c4611af2e86d80a59ac76279043d8f75f454c
react-native-screenguard: 82437eeb0086a90b5e5d7e54130bb04fb406373e
react-native-share-extension: 7528322fe621b683e6b48ab3418207ed45a70983
react-native-sodium: ed020d5f50ab1886f7bf23e32732d0d10e9373ac
react-native-theme-switch-animation: 64f2688c2e3489ababc7b35f436fd3b5ed7c0a98
react-native-webview: a71525b1ab760230fbf37303d8371fbe72051c7d
React-nativeconfig: ba9a2e54e2f0882cf7882698825052793ed4c851
React-NativeModulesApple: 8d11ff8955181540585c944cf48e9e7236952697
React-NativeModulesApple: 84aaad2b0e546d7b839837ca537f6e72804a4cad
React-perflogger: ed4e0c65781521e0424f2e5e40b40cc7879d737e
React-RCTActionSheet: 49d53ff03bb5688ca4606c55859053a0cd129ea5
React-RCTAnimation: 07b4923885c52c397c4ec103924bf6e53b42c73e
React-RCTAppDelegate: 316e295076734baf9bdf1bfac7d92ab647aed930
React-RCTBlob: 85c57b0d5e667ff8a472163ba3af0628171a64bb
React-RCTFabric: 97c1465ded4dc92841f5376a39e43e1b2c455f40
React-RCTImage: b965c85bec820e2a9c154b1fb00a2ecdd59a9c92
React-RCTLinking: 75f04a5f27c26c4e73a39c50df470820d219df79
React-RCTNetwork: c1a9143f4d5778efc92da40d83969d03912ccc24
React-RCTSettings: c6800f91c0ecd48868cd5db754b0b0a7f5ffe039
React-RCTText: b923e24f9b7250bc4f7ab154c4168ad9f8d8fc9d
React-RCTVibration: 08c4f0c917c435b3619386c25a94ee5d64c250f0
React-rendererdebug: 3cda04217d9df67b94397ee0ead8ef3d8b7e427b
React-RCTAnimation: 3075449f26cb98a52bcbf51cccd0c7954e2a71db
React-RCTAppDelegate: 9a419c4dda9dd039ad851411546dd297b930c454
React-RCTBlob: e81ab773a8fc1e9dceed953e889f936a7b7b3aa6
React-RCTFabric: 47a87a3e3fa751674f7e64d0bcd58976b8c57db9
React-RCTImage: d570531201c6dce7b5b63878fa8ecec0cc311c4c
React-RCTLinking: af888972b925d2811633d47853c479e88c35eb4d
React-RCTNetwork: 5728a06ff595003eca628f43f112a804f4a9a970
React-RCTSettings: ba3665b0569714a8aaceee5c7d23b943e333fa55
React-RCTText: b733fa984f0336b072e47512898ba91214f66ddb
React-RCTVibration: 0cbcbbd8781b6f6123671bae9ee5dd20d621af6c
React-rendererdebug: 9fc8f7d0bd19f2a3fe3791982af550b5e1535ff7
React-rncore: 4013508a2f3fcf46c961919bbbd4bfdda198977e
React-RuntimeApple: 447844a2bdb0a03ffd24e5b4a4b96cfc50325b88
React-RuntimeCore: 9b5bffdaccee9b707b1c2694c9044e13ff0bb087
React-RuntimeApple: a852a6e06ab20711658873f39cb10b0033bea19d
React-RuntimeCore: 12e5e176c0cb09926f3e6f37403a84d2e0f203a7
React-runtimeexecutor: 0e688aefc14c6bc8601f4968d8d01c3fb6446844
React-RuntimeHermes: 4d6ef6bb0f2b0b40d59143317f6b99c82764c959
React-runtimescheduler: cfbe85c3510c541ec6dc815c7729b41304b67961
React-utils: f242eb7e7889419d979ca0e1c02ccc0ea6e43b29
ReactCommon: f7da14a8827b72704169a48c929bcde802698361
RNBackgroundFetch: 35c7183f6eeff69396d42c4b206e1ae2a21e9327
RNBootSplash: de2c568373a9c79a66e9918b8929eb6c9a35246f
RNCCheckbox: 43bcc6493611468af0e19f19f029dab3da8561c4
RNCClipboard: 3f0451a8100393908bea5c5c5b16f96d45f30bfc
RNCMaskedView: 949696f25ec596bfc697fc88e6f95cf0c79669b6
RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14
RNDeviceInfo: aad3c663b25752a52bf8fce93f2354001dd185aa
RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
RNFlashList: e9b57a5553639f9b528cc50ab53f25831722ed62
RNGestureHandler: 4fd316f1bdb8d7f6a54efc58da5923cb83014b19
RNIap: 17583f958d35788a87948e94072ff6edd3580497
RNImageCropPicker: d9616a0cb9b72e8551ff94a7a5021fbd29050aa5
RNKeychain: 840f8e6f13be0576202aefcdffd26a4f54bfe7b5
RNNotifee: 1448fd16c2a6f3e8abbc219801bf5be2435183e5
RNPrivacySnapshot: 8eaf571478a353f2e5184f5c803164f22428b023
RNReanimated: 7212582b487bceed64b2dbc6c15777b97922be21
RNScreens: d2a12528d1abe4922e7ee5c9a00dd40c827c2ab5
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
RNShare: 22717e910836a66cb7255b3b8c4ab06cbe346e27
RNSVG: d7d7bc8229af3842c9cfc3a723c815a52cdd1105
RNTooltips: 5424d4bf0b3d441104127943b1115cc7f0616b1f
RNZipArchive: 9d90367b1573903b5561a8c0b911e5cc12626e74
React-RuntimeHermes: 80c03a5215520c9733764ba11cbe535053c9746d
React-runtimescheduler: 2cbd0f3625b30bba08e8768776107f6f0203159b
React-utils: 9fa4e5d0b5e6c6c85c958f19d6ef854337886417
ReactCommon: 9f285823dbe955099978d9bff65a7653ca029256
RNBackgroundFetch: 0c49d5892a0e2f9c139e24ba498985cf0bf070c6
RNBootSplash: 3bc12ecb62f69c5af6e6ddf0e6bef88f572ccdb5
RNCCheckbox: df88c1c78a17869da84bb031c9d8185d417398a6
RNCClipboard: efe1b27ad1ea378c60c8c8aabfd130961bbeb474
RNCMaskedView: 4b5b12efdee55966f1d68f32d4b3401c2acf05c2
RNDateTimePicker: 818460dc31b0dc5ec58289003e27dd8d022fb79c
RNDeviceInfo: d3e91ffb33ee97a7982108476edb68cb3672efa6
RNExitApp: 890cce29b4e01372c84b3b775f9a63f90d77de19
RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251
RNFlashList: fe872a2d1b4c859c64a7d7421628a1308ebb9e4f
RNGestureHandler: 61302deb50c92cba160f5a11f00f69da48116724
RNIap: aa38c2cc8a2a75ba8112d161ca084a0b611955df
RNImageCropPicker: 30d770b383d84e1067d82ea7b5ed4fff851bbbb2
RNKeychain: ffd0513e676445c637410b47249460cbf56bc9cb
RNNotifee: b98fb71f678f6644fa48ec6d515ec6fcfc5214ba
RNPrivacySnapshot: ccad3a548338c2f526bb7b1789af3fb0618b7d1d
RNReanimated: bee9626cf4920f2910bf7c4f8a3dc569161a9475
RNScreens: d0db5e9f28afd284a5193474d6be949f4cba888e
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: 5d7762ded21e1fff97dda6d41e963e22dce2d025
RNSVG: d7cb8bd34550cf4c4fc7edd7ac627905e2b71f3f
RNTooltips: 50ff94bd2bff84ef74279c9532d008f5fdcddb95
RNZipArchive: 71befbdc20e7fbd4b64552a0b324ad0e8d0efefb
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SexyTooltip: 5c9b4dec52bfb317938cb0488efd9da3717bb6fd
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
toolbar-android: 2a73856e98b750d7e71ce4644d3f41cc98211719
toolbar-android: c426ed5bd3dcccfed20fd79533efc0d1ae0ef018
Yoga: 950bbfd7e6f04790fdb51149ed51df41f329fcc8
PODFILE CHECKSUM: aa07aca16c1d92394d8ce12f2c5fcaa8a3ae4997

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.0.27",
"version": "3.0.28",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.0.27",
"version": "3.0.28",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -3821,6 +3821,7 @@
"@lingui/react": "5.1.2",
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.0",
"@notesnook/common": "file:../common",
"@notesnook/editor": "file:../editor",
"@notesnook/intl": "file:../intl",
"@notesnook/theme": "file:../theme",
@@ -3831,6 +3832,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-freeze": "^1.0.3",
"tinycolor2": "1.6.0",
"zustand": "^4.4.7"
},
"devDependencies": {
@@ -7740,7 +7742,7 @@
},
"../../packages/editor-mobile/node_modules/@types/prop-types": {
"version": "15.7.11",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/q": {
@@ -7760,7 +7762,7 @@
},
"../../packages/editor-mobile/node_modules/@types/react": {
"version": "18.2.39",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -7791,7 +7793,7 @@
},
"../../packages/editor-mobile/node_modules/@types/scheduler": {
"version": "0.16.8",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/semver": {
@@ -12616,7 +12618,7 @@
},
"../../packages/editor-mobile/node_modules/immer": {
"version": "9.0.21",
"devOptional": true,
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -23088,6 +23090,7 @@
},
"../../packages/editor/node_modules/js-tokens": {
"version": "4.0.0",
"dev": true,
"license": "MIT"
},
"../../packages/editor/node_modules/jsesc": {
@@ -23138,6 +23141,7 @@
},
"../../packages/editor/node_modules/loose-envify": {
"version": "1.4.0",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -23649,6 +23653,7 @@
},
"../../packages/editor/node_modules/react": {
"version": "18.3.1",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -23667,6 +23672,7 @@
},
"../../packages/editor/node_modules/react-dom": {
"version": "18.3.1",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
@@ -23805,6 +23811,7 @@
},
"../../packages/editor/node_modules/scheduler": {
"version": "0.23.2",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -28268,7 +28275,12 @@
"@readme/data-urls": "3.0.0",
"@streetwriters/kysely": "^0.27.4",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@tanstack/react-query": "^4.36.1",
"@trpc/client": "^10.45.2",
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"absolutify": "^0.1.0",
"async-mutex": "0.5.0",
"buffer": "^6.0.3",
"dayjs": "^1.11.13",
"deprecated-react-native-prop-types": "^4.1.0",
@@ -28582,6 +28594,7 @@
},
"node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.22.5"
@@ -28701,6 +28714,7 @@
},
"node_modules/@babel/helper-hoist-variables": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.22.5"
@@ -28960,6 +28974,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz",
"integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
@@ -28974,6 +28989,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz",
"integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
@@ -29122,6 +29138,7 @@
"version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
"dev": true,
"engines": {
"node": ">=6.9.0"
},
@@ -29134,6 +29151,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz",
"integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==",
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.6"
@@ -29168,6 +29186,7 @@
},
"node_modules/@babel/plugin-syntax-class-properties": {
"version": "7.12.13",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.12.13"
@@ -29180,6 +29199,7 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
"integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
},
@@ -29217,6 +29237,7 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz",
"integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.3"
},
@@ -29241,6 +29262,7 @@
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz",
"integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29255,6 +29277,7 @@
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz",
"integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29269,6 +29292,7 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
"integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
},
@@ -29280,6 +29304,7 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
"integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
},
@@ -29379,6 +29404,7 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
"integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
},
@@ -29406,6 +29432,7 @@
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.6"
@@ -29434,6 +29461,7 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz",
"integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -29464,6 +29492,7 @@
},
"node_modules/@babel/plugin-transform-block-scoped-functions": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29492,6 +29521,7 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz",
"integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==",
"dev": true,
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29507,6 +29537,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz",
"integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==",
"dev": true,
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -29571,6 +29602,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz",
"integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29586,6 +29618,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz",
"integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29600,6 +29633,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz",
"integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
@@ -29612,6 +29646,7 @@
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5",
@@ -29628,6 +29663,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz",
"integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-export-namespace-from": "^7.8.3"
@@ -29655,6 +29691,7 @@
},
"node_modules/@babel/plugin-transform-for-of": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29685,6 +29722,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz",
"integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-json-strings": "^7.8.3"
@@ -29713,6 +29751,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz",
"integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
@@ -29726,6 +29765,7 @@
},
"node_modules/@babel/plugin-transform-member-expression-literals": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29741,6 +29781,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz",
"integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==",
"dev": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29771,6 +29812,7 @@
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz",
"integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==",
"dev": true,
"dependencies": {
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-module-transforms": "^7.22.5",
@@ -29788,6 +29830,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz",
"integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==",
"dev": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29817,6 +29860,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz",
"integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29846,6 +29890,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz",
"integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-numeric-separator": "^7.10.4"
@@ -29861,6 +29906,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz",
"integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.5",
@@ -29877,6 +29923,7 @@
},
"node_modules/@babel/plugin-transform-object-super": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -29893,6 +29940,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz",
"integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
@@ -29966,6 +30014,7 @@
},
"node_modules/@babel/plugin-transform-property-literals": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30035,6 +30084,7 @@
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -30051,6 +30101,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz",
"integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -30143,6 +30194,7 @@
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz",
"integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -30173,6 +30225,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz",
"integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -30187,6 +30240,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz",
"integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30216,6 +30270,7 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz",
"integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30231,6 +30286,7 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz",
"integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.5",
@@ -30324,6 +30380,7 @@
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz",
"integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
@@ -30339,6 +30396,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -33642,7 +33700,8 @@
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
@@ -33748,14 +33807,14 @@
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -34111,6 +34170,7 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
"integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
"dev": true,
"dependencies": {
"@webassemblyjs/helper-numbers": "1.11.6",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6"
@@ -34119,22 +34179,26 @@
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
"integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="
"integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
"dev": true
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
"dev": true
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
"integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw=="
"integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==",
"dev": true
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
"integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
"dev": true,
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.11.6",
"@webassemblyjs/helper-api-error": "1.11.6",
@@ -34144,12 +34208,14 @@
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
"dev": true
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
"integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-buffer": "1.11.6",
@@ -34161,6 +34227,7 @@
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
"integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
"dev": true,
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
}
@@ -34169,6 +34236,7 @@
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
"integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
"dev": true,
"dependencies": {
"@xtuc/long": "4.2.2"
}
@@ -34176,12 +34244,14 @@
"node_modules/@webassemblyjs/utf8": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
"dev": true
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
"integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-buffer": "1.11.6",
@@ -34197,6 +34267,7 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
"integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
@@ -34209,6 +34280,7 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
"integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-buffer": "1.11.6",
@@ -34220,6 +34292,7 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
"integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-api-error": "1.11.6",
@@ -34233,6 +34306,7 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
"integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@xtuc/long": "4.2.2"
@@ -34290,12 +34364,14 @@
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
"dev": true
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
@@ -34596,6 +34672,14 @@
"version": "1.0.1",
"license": "MIT"
},
"node_modules/async-mutex": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/at-least-node": {
"version": "1.0.0",
"dev": true,
@@ -35286,6 +35370,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
"dev": true,
"engines": {
"node": ">=6.0"
}
@@ -35716,7 +35801,7 @@
},
"node_modules/csstype": {
"version": "3.1.2",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/date-fns": {
@@ -36316,6 +36401,7 @@
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -36435,7 +36521,8 @@
"node_modules/es-module-lexer": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw=="
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
"dev": true
},
"node_modules/es-object-atoms": {
"version": "1.0.0",
@@ -36780,6 +36867,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
@@ -36792,6 +36880,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true,
"engines": {
"node": ">=4.0"
}
@@ -36955,6 +37044,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -36966,6 +37056,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"engines": {
"node": ">=4.0"
}
@@ -36974,6 +37065,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -37733,7 +37825,8 @@
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true
},
"node_modules/global": {
"version": "4.4.0",
@@ -39822,7 +39915,8 @@
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"node_modules/json-schema-ref-resolver": {
"version": "1.0.1",
@@ -40298,6 +40392,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
"dev": true,
"engines": {
"node": ">=6.11.5"
}
@@ -42527,6 +42622,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
@@ -42548,30 +42644,6 @@
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/react-dom/node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/react-freeze": {
"version": "1.0.3",
"license": "MIT",
@@ -43365,6 +43437,7 @@
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz",
"integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-is": "^18.2.0",
"react-shallow-renderer": "^16.15.0",
@@ -43490,6 +43563,7 @@
},
"node_modules/regenerator-transform": {
"version": "0.15.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.4"
@@ -43842,6 +43916,7 @@
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
@@ -44499,6 +44574,7 @@
},
"node_modules/tapable": {
"version": "2.2.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -44576,6 +44652,7 @@
"version": "5.3.10",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
"jest-worker": "^27.4.5",
@@ -44609,6 +44686,7 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
"dev": true,
"dependencies": {
"@types/node": "*",
"merge-stream": "^2.0.0",
@@ -44622,6 +44700,7 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -45198,6 +45277,7 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
"dev": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
@@ -45221,6 +45301,7 @@
"version": "5.94.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.0",
@@ -45336,6 +45417,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true,
"engines": {
"node": ">=10.13.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.0.27",
"version": "3.0.30",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

@@ -26,7 +26,7 @@ type ErrorsListProps = {
export function ErrorsList(props: ErrorsListProps) {
return (
<Accordion
title={`${props.errors.length} errors occured`}
title={`${props.errors.length} errors occurred`}
sx={{ bg: "errorBg", borderRadius: "default", mt: 2 }}
color="#E53935"
>

View File

@@ -55,7 +55,7 @@ export class AppModel {
}
goBack() {
const goBackButton = this.page.locator(getTestId("go-back"));
const goBackButton = this.page.locator(getTestId("route-go-back"));
return goBackButton.click();
}

View File

@@ -39,10 +39,12 @@ export class BaseItemModel {
return await this.locator.evaluate((el) => el === document.activeElement);
}
async click() {
async click(options?: { middleClick?: boolean }) {
if (!(await this.locator.isVisible()))
await this.locator.scrollIntoViewIfNeeded();
await this.locator.click();
await this.locator.click({
button: options?.middleClick ? "middle" : "left"
});
}
async getId() {

View File

@@ -36,6 +36,9 @@ export class EditorModel {
private readonly dateEditedText: Locator;
private readonly searchButton: Locator;
private readonly tabsList: Locator;
private readonly goBackButton: Locator;
private readonly goForwardButton: Locator;
private readonly newTabButton: Locator;
readonly savedIcon: Locator;
readonly notSavedIcon: Locator;
@@ -59,6 +62,9 @@ export class EditorModel {
this.savedIcon = page.locator(getTestId("editor-save-state-saved"));
this.notSavedIcon = page.locator(getTestId("editor-save-state-notsaved"));
this.tabsList = page.locator(getTestId("tabs"));
this.goBackButton = page.locator(getTestId("go-back"));
this.goForwardButton = page.locator(getTestId("go-forward"));
this.newTabButton = page.locator(getTestId("New tab"));
}
async waitForLoading(title?: string, content?: string) {
@@ -243,6 +249,26 @@ export class EditorModel {
}
}
async getTabs() {
const tabs: TabItemModel[] = [];
for await (const item of iterateList(this.tabsList.locator(".tab"))) {
tabs.push(new TabItemModel(item, this.page));
}
return tabs;
}
async goBack() {
await this.goBackButton.click();
}
async goForward() {
await this.goForwardButton.click();
}
async newTab() {
await this.newTabButton.click();
}
async attachImage() {
await this.page
.context()

View File

@@ -38,8 +38,8 @@ export class NoteItemModel extends BaseItemModel {
this.editor = new EditorModel(this.page);
}
async openNote() {
await this.click();
async openNote(openInNewTab?: boolean) {
await this.click({ middleClick: openInNewTab });
const title = await this.getTitle();
await this.editor.waitForLoading(title);
}

View File

@@ -211,6 +211,11 @@ export class NoteContextMenuModel extends BaseProperties {
this.menu = new ContextMenuModel(page);
}
async openInNewTab() {
await this.open();
await this.menu.clickOnItem("openinnewtab");
}
async isColored(color: string): Promise<boolean> {
await this.open();
await this.menu.clickOnItem("colors");

View File

@@ -30,6 +30,20 @@ export class TabItemModel {
const testId = await this.locator.getAttribute("data-test-id");
return testId?.replace("tab-", "");
}
async click() {
return this.locator.click();
}
async title() {
return this.locator.locator(getTestId("tab-title")).textContent();
}
async isActive() {
const classList = await this.locator.getAttribute("class");
return !!classList?.includes("active");
}
close() {
return this.closeButton.click();
}

View File

@@ -17,47 +17,11 @@ 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 { test, expect, Page } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { NOTE, PASSWORD } from "./utils";
import { test, expect } from "@playwright/test";
import { createHistorySession, PASSWORD } from "./utils";
test.setTimeout(60 * 1000);
async function createSession(page: Page, locked = false) {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
let note = await notes.createNote(NOTE);
if (locked) {
await note?.contextMenu.lock(PASSWORD);
await note?.openLockedNote(PASSWORD);
}
const edits = ["Some edited text.", "Some more edited text."];
for (const edit of edits) {
await notes.editor.setContent(edit);
await page.waitForTimeout(600);
await page.reload().catch(console.error);
await notes.waitForItem(NOTE.title);
note = await notes.findNote(NOTE);
locked ? await note?.openLockedNote(PASSWORD) : await note?.openNote();
}
const contents = [
`${NOTE.content}${edits[0]}${edits[1]}`,
`${NOTE.content}${edits[0]}`
];
return {
note,
notes,
app,
contents
};
}
const sessionTypes = ["locked", "unlocked"] as const;
for (const type of sessionTypes) {
@@ -66,7 +30,7 @@ for (const type of sessionTypes) {
test(`editing a note should create a new ${type} session in its session history`, async ({
page
}) => {
const { note } = await createSession(page, isLocked);
const { note } = await createHistorySession(page, isLocked);
const history = await note?.properties.getSessionHistory();
expect(history?.length).toBeGreaterThan(1);
@@ -81,7 +45,7 @@ for (const type of sessionTypes) {
test(`switching ${type} sessions should change editor content`, async ({
page
}) => {
const { note, contents } = await createSession(page, isLocked);
const { note, contents } = await createHistorySession(page, isLocked);
const history = await note?.properties.getSessionHistory();
let preview = await history?.at(1)?.open();
@@ -91,6 +55,7 @@ for (const type of sessionTypes) {
contents[1]
);
await note?.click();
if (type === "locked") await note?.openLockedNote(PASSWORD);
await note?.properties.close();
preview = await history?.at(0)?.open();
if (type === "locked") await preview?.unlock(PASSWORD);
@@ -103,7 +68,10 @@ for (const type of sessionTypes) {
test(`restoring a ${type} session should change note's content`, async ({
page
}) => {
const { note, notes, contents } = await createSession(page, isLocked);
const { note, notes, contents } = await createHistorySession(
page,
isLocked
);
const history = await note?.properties.getSessionHistory();
const preview = await history?.at(1)?.open();
if (type === "locked") await preview?.unlock(PASSWORD);
@@ -115,44 +83,3 @@ for (const type of sessionTypes) {
expect(await notes.editor.getContent("text")).toBe(contents[1]);
});
}
// test("editing locked note should create locked history sessions", async ({
// page
// }) => {
// const { note } = await createLockedSession(page);
// const history = await note?.properties.getSessionHistory();
// expect(history).toHaveLength(2);
// for (const item of history || []) {
// expect(await item.isLocked()).toBeTruthy();
// }
// });
// test("switching locked sessions should change editor content", async ({
// page
// }) => {
// const { note, notes, contents } = await createLockedSession(page);
// const history = await note?.properties.getSessionHistory();
// await history?.at(1)?.previewLocked(PASSWORD);
// await notes.editor.waitForLoading(NOTE.title, contents[1]);
// const content1 = await notes.editor.getContent("text");
// await history?.at(0)?.previewLocked(PASSWORD);
// await notes.editor.waitForLoading(NOTE.title, contents[0]);
// const content0 = await notes.editor.getContent("text");
// expect(content1).toBe(contents[1]);
// expect(content0).toBe(contents[0]);
// });
// test("restore a locked session", async ({ page }) => {
// const { note, notes, contents } = await createLockedSession(page);
// const history = await note?.properties.getSessionHistory();
// await history?.at(1)?.previewLocked(PASSWORD);
// await notes.editor.waitForLoading(NOTE.title, contents[1]);
// await notes.editor.restoreSession();
// const content = await notes.editor.getContent("text");
// expect(content).toBe(contents[1]);
// });

View File

@@ -20,6 +20,9 @@ import { test, Browser, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { USER } from "./utils";
// run this test file sequentially
test.describe.configure({ mode: "serial" });
async function createDevice(browser: Browser) {
// Create two isolated browser contexts
const context = await browser.newContext();
@@ -48,13 +51,13 @@ async function actAndSync<T>(
return results.slice(0, actions.length) as T[];
}
const NOTE = {
title: `Note ${makeid(20)}`
};
test(`edits in a note opened on 2 devices should sync in real-time`, async ({
test(`content edits in a note opened on 2 devices in multiple tabs should sync in real-time`, async ({
browser
}, info) => {
const NOTE = {
title: `Note ${makeid(20)}`
};
info.setTimeout(70 * 1000);
const newContent = makeid(24).repeat(2);
@@ -71,6 +74,7 @@ test(`edits in a note opened on 2 devices should sync in real-time`, async ({
)[0];
const noteA = await notesA.findNote(NOTE);
await Promise.all([noteA, noteB].map((note) => note?.openNote()));
await noteA?.openNote(true);
if ((await notesB.editor.getContent("text")) !== "")
await actAndSync([deviceA, deviceB], notesB.editor.clear());
@@ -82,6 +86,52 @@ test(`edits in a note opened on 2 devices should sync in real-time`, async ({
expect(noteB).toBeDefined();
await expect(notesA.editor.content).toHaveText(newContent);
await expect(notesB.editor.content).toHaveText(newContent);
const tabsA = await notesA.editor.getTabs();
await tabsA[0].click();
await expect(notesA.editor.content).toHaveText(newContent);
await (await deviceA.goToSettings())?.logout();
await (await deviceB.goToSettings())?.logout();
});
test(`title edits in a note opened on 2 devices in multiple tabs should sync in real-time`, async ({
browser
}, info) => {
const NOTE = {
title: `Note ${makeid(20)}`
};
info.setTimeout(70 * 1000);
const newContent = makeid(24).repeat(2);
const [deviceA, deviceB] = await Promise.all([
createDevice(browser),
createDevice(browser)
]);
const [notesA, notesB] = await Promise.all(
[deviceA, deviceB].map((d) => d.goToNotes())
);
const noteB = (
await actAndSync([deviceA, deviceB], notesB.createNote(NOTE))
)[0];
const noteA = await notesA.findNote(NOTE);
await Promise.all([noteA, noteB].map((note) => note?.openNote()));
await noteA?.openNote(true);
if ((await notesB.editor.getContent("text")) !== "")
await actAndSync([deviceA, deviceB], notesB.editor.clear());
await expect(notesA.editor.content).toBeEmpty();
await expect(notesB.editor.content).toBeEmpty();
await actAndSync([deviceA, deviceB], notesB.editor.setTitle(newContent));
expect(noteA).toBeDefined();
expect(noteB).toBeDefined();
expect(await notesA.editor.getTitle()).toBe(newContent);
expect(await notesB.editor.getTitle()).toBe(newContent);
const tabsA = await notesA.editor.getTabs();
await tabsA[0].click();
expect(await notesA.editor.getTitle()).toBe(newContent);
await (await deviceA.goToSettings())?.logout();
await (await deviceB.goToSettings())?.logout();

View File

@@ -0,0 +1,256 @@
/*
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 { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { createHistorySession } from "./utils";
test("notes should open in the same tab", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({ title: "Note 1" });
await notes.createNote({ title: "Note 2" });
await notes.createNote({ title: "Note 3" });
await page.reload();
const note = await notes.findNote({ title: "Note 2" });
await note?.click();
const tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(1);
});
test("new note should open in the same tab", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({ title: "Note 1" });
await notes.newNote();
const tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(1);
});
test("open note in new tab (using context menu)", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({ title: "Note 1" });
await notes.createNote({ title: "Note 2" });
await notes.createNote({ title: "Note 3" });
const note = await notes.findNote({ title: "Note 2" });
await note?.contextMenu.openInNewTab();
await notes.editor.waitForLoading();
const tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(2);
expect(await tabs[1].title()).toBe("Note 2");
});
test("open note in new tab (using middle click)", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({ title: "Note 1" });
await notes.createNote({ title: "Note 2" });
await notes.createNote({ title: "Note 3" });
const note = await notes.findNote({ title: "Note 2" });
await note?.click({ middleClick: true });
await notes.editor.waitForLoading();
const tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(2);
expect(await tabs[1].title()).toBe("Note 2");
});
test("go back should open previous note", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const tabs = await notes.editor.getTabs();
await notes.createNote({ title: "Note 1" });
await notes.createNote({ title: "Note 2" });
await notes.createNote({ title: "Note 3" });
await notes.editor.goBack();
expect(await tabs[0].title()).toBe("Note 2");
await notes.editor.goBack();
expect(await tabs[0].title()).toBe("Note 1");
});
test("go forward should open next note", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const tabs = await notes.editor.getTabs();
await notes.createNote({ title: "Note 1" });
await notes.createNote({ title: "Note 2" });
await notes.createNote({ title: "Note 3" });
await notes.editor.goBack();
await notes.editor.goBack();
await notes.editor.goForward();
expect(await tabs[0].title()).toBe("Note 2");
await notes.editor.goForward();
expect(await tabs[0].title()).toBe("Note 3");
});
test("new tab button should open a new tab", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({ title: "Note 1" });
await notes.editor.newTab();
const tabs = await notes.editor.getTabs();
expect(await tabs[0].title()).toBe("Note 1");
expect(await tabs[1].title()).toBe("Untitled");
});
test("changes in a note opened in multiple tabs should sync", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({ title: "Note 1" });
await note?.contextMenu.openInNewTab();
await notes.editor.editAndWait(async () => {
await notes.editor.setContent("This change should sync.");
});
const tabs = await notes.editor.getTabs();
await tabs[0].click();
expect(await notes.editor.getContent("text")).toBe(
"This change should sync."
);
});
test("open same note in 2 tabs and refresh page", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({
title: "Note 1",
content: "Some edits."
});
await note?.contextMenu.openInNewTab();
await notes.editor.waitForLoading();
await page.reload();
await notes.waitForList();
const tabs = await notes.editor.getTabs();
await tabs[0].click();
await notes.editor.waitForLoading();
expect(await notes.editor.getContent("text")).toBe("Some edits.");
});
test("reloading with a note diff open in a tab", async ({ page }) => {
const { note, contents } = await createHistorySession(page);
const history = await note?.properties.getSessionHistory();
const preview = await history?.[0].open();
await preview!.firstEditor.waitFor({ state: "visible" });
await page.reload();
await preview!.firstEditor.waitFor({ state: "visible" });
await expect(preview!.firstEditor.locator(".ProseMirror")).toHaveText(
contents[0]
);
await expect(preview!.secondEditor.locator(".ProseMirror")).toHaveText(
contents[0]
);
});
test("navigate back and forth between normal and diff session", async ({
page
}) => {
const { note, contents, notes } = await createHistorySession(page);
const history = await note?.properties.getSessionHistory();
const preview = await history?.[0].open();
await preview!.firstEditor.waitFor({ state: "visible" });
await notes.editor.goBack();
await preview!.firstEditor.waitFor({ state: "hidden" });
expect(await notes.editor.getContent("text")).toBe(contents[0]);
await notes.editor.goForward();
await preview!.firstEditor.waitFor({ state: "visible" });
await expect(preview!.firstEditor.locator(".ProseMirror")).toHaveText(
contents[0]
);
await expect(preview!.secondEditor.locator(".ProseMirror")).toHaveText(
contents[0]
);
});
test("clicking on a note that's already opened in another tab should focus the tab", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({
title: "Note 1"
});
await note?.contextMenu.openInNewTab();
await notes.editor.waitForLoading();
await notes.createNote({
title: "Note 2"
});
await note?.openNote();
const tabs = await notes.editor.getTabs();
expect(await tabs[0].isActive()).toBe(true);
});
test("open a note in 2 tabs then open another note and navigate back", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({
title: "Note 1"
});
await note?.contextMenu.openInNewTab();
await notes.editor.waitForLoading();
await notes.createNote({
title: "Note 2"
});
await notes.editor.goBack();
const tabs = await notes.editor.getTabs();
expect(await tabs[1].isActive()).toBe(true);
expect(await tabs[1].title()).toBe("Note 1");
});
test.skip("TODO: open a locked note, switch to another note and navigate back", () => {});
test.skip("TODO: open a locked note, switch to another note, unlock the note and navigate back", () => {});

View File

@@ -28,6 +28,7 @@ import {
SortByOptions
} from "../models/types";
import { tmpdir } from "os";
import { AppModel } from "../models/app.model";
type Note = {
title: string;
@@ -74,7 +75,10 @@ const PASSWORD = "123abc123abc";
const APP_LOCK_PASSWORD = "lockapporelse🔪";
function getTestId(id: string, variant: "data-test-id" | "data-testid" = "data-test-id") {
function getTestId(
id: string,
variant: "data-test-id" | "data-testid" = "data-test-id"
) {
return `[${variant}="${id}"]`;
}
@@ -153,6 +157,41 @@ const groupByOptions: GroupByOptions[] = [
"week"
];
export async function createHistorySession(page: Page, locked = false) {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
let note = await notes.createNote(NOTE);
if (locked) {
await note?.contextMenu.lock(PASSWORD);
await note?.openLockedNote(PASSWORD);
}
const edits = ["Some edited text.", "Some more edited text."];
for (const edit of edits) {
await notes.editor.setContent(edit);
await page.waitForTimeout(600);
await page.reload().catch(console.error);
await notes.waitForItem(NOTE.title);
note = await notes.findNote(NOTE);
locked ? await note?.openLockedNote(PASSWORD) : await note?.openNote();
}
const contents = [
`${NOTE.content}${edits[0]}${edits[1]}`,
`${NOTE.content}${edits[0]}`
];
return {
note,
notes,
app,
contents
};
}
export {
USER,
NOTE,

View File

@@ -36,7 +36,7 @@ The flow will be as follows:
5. Use the database password to init the db & keep the user's encryption key in memory. (we have 2 choices here: either keep the encryption key in memory or keep the pin in memory)
6. If not enabled, we will ask the keystore to give us the database password + user's encryption key
We should store both the database password and the user's encryption key in a keychain. If user sets a pin, we can just double encrypt these values. This will keep things simple and seemless.
We should store both the database password and the user's encryption key in a keychain. If user sets a pin, we can just double encrypt these values. This will keep things simple and seamless.
Q: How to find out if app lock is enabled?

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "3.0.24",
"version": "3.0.25",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -64,8 +64,8 @@ function App() {
{isFocused ? null : (
<Global
styles={`
body {
filter: brightness(0.95) grayscale(1);
.nav-pane {
opacity: 0.7;
}
.titlebar {
background: var(--background-secondary) !important;
@@ -150,23 +150,9 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
useEffect(() => {
if (isTablet) navPane.current?.collapse(0);
else navPane.current?.expand(0);
else if (navPane.current?.isCollapsed(0)) navPane.current?.expand(0);
}, [isTablet]);
// useEffect(() => {
// if (show) middlePane.current?.expand();
// else middlePane.current?.collapse();
// }, [show]);
// useEffect(() => {
// if (isFocusMode) {
// const middlePaneSize = middlePane.current?.getSize() || 20;
// navPane.current?.collapse();
// // the middle pane has to be resized because collapsing the nav
// // pane increases the middle pane's size every time.
// middlePane.current?.resize(middlePaneSize);
// } else navPane.current?.expand();
// }, [isFocusMode]);
return (
<>
<Flex

View File

@@ -18,11 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import hotkeys from "hotkeys-js";
import { GlobalKeyboard } from "../utils/keyboard";
import { useEditorStore } from "../stores/editor-store";
import { useStore as useSearchStore } from "../stores/search-store";
import { useEditorManager } from "../components/editor/manager";
function isInEditor(e: KeyboardEvent) {
return (
e.target instanceof HTMLElement && !!e.target?.closest(".editor-container")
);
}
const KEYMAP = [
// {
// keys: ["command+n", "ctrl+n", "command+alt+n", "ctrl+alt+n"],
@@ -54,15 +59,56 @@ const KEYMAP = [
// });
// },
// },
{
keys: [
"command+option+right",
"ctrl+alt+right",
"command+option+shift+right",
"ctrl+alt+shift+right",
"ctrl+tab",
"command+tab"
],
description: "Go to next tab",
action: () => useEditorStore.getState().focusNextTab()
},
{
keys: [
"command+option+left",
"ctrl+alt+left",
"command+option+shift+left",
"ctrl+alt+shift+left",
"ctrl+shift+tab",
"command+shift+tab"
],
description: "Go to previous tab",
action: () => useEditorStore.getState().focusPreviousTab()
},
{
keys: ["ctrl+t", "command+t"],
description: "Create a new tab",
action: () => useEditorStore.getState().addTab()
},
{
keys: ["ctrl+n", "command+n"],
description: "Create a new note",
action: () => useEditorStore.getState().newSession()
},
{
keys: ["ctrl+w", "command+w"],
description: "Close active tab",
action: () => useEditorStore.getState().closeActiveTab()
},
{
keys: ["ctrl+shift+w", "command+shift+w"],
description: "Close all tabs",
action: () => useEditorStore.getState().closeAllTabs()
},
{
keys: ["command+f", "ctrl+f"],
description: "Search all notes",
global: false,
action: (e: KeyboardEvent) => {
const isInEditor =
e.target instanceof HTMLElement &&
!!e.target?.closest(".editor-container");
if (isInEditor) {
if (isInEditor(e)) {
const activeSession = useEditorStore.getState().getActiveSession();
if (activeSession?.type === "readonly") {
e.preventDefault();
@@ -149,14 +195,9 @@ export function registerKeyMap() {
};
KEYMAP.forEach((key) => {
hotkeys(
key.keys.join(","),
{
element: key.global
? (GlobalKeyboard as unknown as HTMLElement)
: document.body
},
key.action
);
hotkeys(key.keys.join(","), (e) => {
e.preventDefault();
key.action?.(e);
});
});
}

View File

@@ -165,7 +165,7 @@ export class Base {
}
/**
* Handle asynchronous operation. This implementation will be overriden on
* Handle asynchronous operation. This implementation will be overridden on
* registration by an Asyncify build.
* @param {function(): Promise<number>} f
* @returns {number}

View File

@@ -269,7 +269,7 @@ export interface SQLiteModule {
* the corresponding Javascript wrapper will throw an exception with a
* `code` property on an error.
*
* Note that a few functions return a Promise in order to accomodate
* Note that a few functions return a Promise in order to accommodate
* either a synchronous or asynchronous SQLite build, generally those
* involved with opening/closing a database or executing a statement.
*
@@ -493,7 +493,7 @@ export interface SQLiteAPI {
column(stmt: number, i: number): SQLiteCompatibleType;
/**
* Extract a column value from a row after a prepared statment {@link step}
* Extract a column value from a row after a prepared statement {@link step}
*
* The contents of the returned buffer may be invalid after the
* next SQLite call. Make a copy of the data (e.g. with `.slice()`)
@@ -523,7 +523,7 @@ export interface SQLiteAPI {
column_count(stmt: number): number;
/**
* Extract a column value from a row after a prepared statment {@link step}
* Extract a column value from a row after a prepared statement {@link step}
* @see https://www.sqlite.org/c3ref/column_blob.html
* @param stmt prepared statement pointer
* @param i column index
@@ -532,7 +532,7 @@ export interface SQLiteAPI {
column_double(stmt: number, i: number): number;
/**
* Extract a column value from a row after a prepared statment {@link step}
* Extract a column value from a row after a prepared statement {@link step}
* @see https://www.sqlite.org/c3ref/column_blob.html
* @param stmt prepared statement pointer
* @param i column index
@@ -541,7 +541,7 @@ export interface SQLiteAPI {
column_int(stmt: number, i: number): number;
/**
* Extract a column value from a row after a prepared statment {@link step}
* Extract a column value from a row after a prepared statement {@link step}
* @see https://www.sqlite.org/c3ref/column_blob.html
* @param stmt prepared statement pointer
* @param i column index
@@ -569,7 +569,7 @@ export interface SQLiteAPI {
column_names(stmt: number): Array<string>;
/**
* Extract a column value from a row after a prepared statment {@link step}
* Extract a column value from a row after a prepared statement {@link step}
* @see https://www.sqlite.org/c3ref/column_blob.html
* @param stmt prepared statement pointer
* @param i column index

View File

@@ -17,7 +17,13 @@ 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, useLayoutEffect, useRef, useState } from "react";
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState
} from "react";
import { Flex, Text, Button } from "@theme-ui/components";
import { Copy, Restore } from "../icons";
import ContentToggle from "./content-toggle";
@@ -25,6 +31,7 @@ import { store as notesStore } from "../../stores/note-store";
import { db } from "../../common/db";
import {
ConflictedEditorSession,
DiffEditorSession,
useEditorStore
} from "../../stores/editor-store";
import { ScrollSync, ScrollSyncPane } from "react-scroll-sync";
@@ -35,7 +42,7 @@ import { getFormattedDate } from "@notesnook/common";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
type DiffViewerProps = { session: ConflictedEditorSession };
type DiffViewerProps = { session: ConflictedEditorSession | DiffEditorSession };
function DiffViewer(props: DiffViewerProps) {
const { session } = props;
@@ -44,7 +51,11 @@ function DiffViewer(props: DiffViewerProps) {
const [conflictedContent, setConflictedContent] = useState(
content?.conflicted
);
const root = useRef<HTMLDivElement>(null);
useEffect(() => {
setContent(session.content);
setConflictedContent(session.content?.conflicted);
}, [session.content]);
const onResolveContent = useCallback(
(saveCopy: boolean) => {
@@ -71,19 +82,13 @@ function DiffViewer(props: DiffViewerProps) {
});
}, [session]);
useLayoutEffect(() => {
const element = root.current;
element?.classList.add("active");
return () => {
element?.classList.remove("active");
};
}, []);
if (!conflictedContent || !content) return null;
return (
<Flex
ref={root}
ref={(el) => {
if (el) el.classList.add("active");
}}
className="diffviewer"
data-test-id="diff-viewer"
sx={{
@@ -112,12 +117,15 @@ function DiffViewer(props: DiffViewerProps) {
variant="secondary"
data-test-id="restore-session"
onClick={async () => {
const { closeSessions, openSession } =
const { closeTabs, openSession, getSessionsForNote } =
useEditorStore.getState();
await db.noteHistory.restore(session.id);
await db.noteHistory.restore(session.historySessionId);
closeSessions(session.id, session.note.id);
closeTabs(
session.id,
...getSessionsForNote(session.note.id).map((s) => s.id)
);
await notesStore.refresh();
await openSession(session.note.id, { force: true });
@@ -135,12 +143,11 @@ function DiffViewer(props: DiffViewerProps) {
<Button
variant="secondary"
onClick={async () => {
const { closeSessions, openSession } =
useEditorStore.getState();
const { closeTabs, openSession } = useEditorStore.getState();
const noteId = await createCopy(session.note, content);
closeSessions(session.id);
closeTabs(session.id);
await notesStore.refresh();
await openSession(noteId);

View File

@@ -18,9 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Button, Flex, Text } from "@theme-ui/components";
import { useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import {
ArrowLeft,
ArrowRight,
Cross,
EditorFullWidth,
EditorNormalWidth,
@@ -28,10 +29,12 @@ import {
FocusMode,
Fullscreen,
Lock,
NewTab,
NormalMode,
Note,
NoteRemove,
Pin,
Plus,
Properties,
Publish,
Published,
@@ -86,10 +89,10 @@ import { isMac } from "../../utils/platform";
export function EditorActionBar() {
const { isMaximized, isFullscreen, hasNativeWindowControls } =
useWindowControls();
const editorMargins = useEditorStore((store) => store.editorMargins);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const activeTab = useEditorStore((store) => store.getActiveTab());
const activeSession = useEditorStore((store) =>
store.activeSessionId ? store.getSession(store.activeSessionId) : undefined
activeTab ? store.getSession(activeTab.sessionId) : undefined
);
const editorManager = useEditorManager((store) =>
activeSession?.id ? store.editors[activeSession?.id] : undefined
@@ -97,11 +100,19 @@ export function EditorActionBar() {
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
const monographs = useMonographStore((store) => store.monographs);
const isNotePublished =
activeSession && db.monographs.isPublished(activeSession.id);
activeSession &&
"note" in activeSession &&
db.monographs.isPublished(activeSession.note.id);
const isMobile = useMobile();
const isTablet = useTablet();
const tools = [
{
title: strings.newTab(),
icon: NewTab,
enabled: true,
onClick: () => useEditorStore.getState().addTab()
},
{
title: strings.undo(),
icon: Undo,
@@ -128,44 +139,6 @@ export function EditorActionBar() {
activeSession.type === "readonly") &&
showPublishView(activeSession.note, "top")
},
{
title: editorMargins
? strings.disableEditorMargins()
: strings.enableEditorMargins(),
icon: editorMargins ? EditorNormalWidth : EditorFullWidth,
enabled: true,
hideOnMobile: true,
onClick: () => useEditorStore.getState().toggleEditorMargins()
},
{
title: isFullscreen
? strings.exitFullScreen()
: strings.enterFullScreen(),
icon: isFullscreen ? ExitFullscreen : Fullscreen,
enabled: true,
hidden: !isFocusMode,
hideOnMobile: true,
onClick: () => {
if (isFullscreen) {
exitFullscreen();
} else {
enterFullscreen(document.documentElement);
}
}
},
{
title: isFocusMode ? strings.normalMode() : strings.focusMode(),
icon: isFocusMode ? FocusMode : NormalMode,
enabled: true,
hideOnMobile: true,
onClick: () => {
useAppStore.getState().toggleFocusMode();
if (document.fullscreenElement) exitFullscreen();
const id = useEditorStore.getState().activeSessionId;
const editor = id && useEditorManager.getState().getEditor(id);
if (editor) editor.editor?.focus();
}
},
{
title: strings.toc(),
icon: TableOfContents,
@@ -238,7 +211,10 @@ export function EditorActionBar() {
mr:
hasNativeWindowControls && !isMac() && !isMobile && !isTablet
? `calc(100vw - env(titlebar-area-width))`
: 0
: 1,
pl: 1,
borderLeft: "1px solid var(--border)",
flexShrink: 0
}}
>
{tools.map((tool) => (
@@ -249,14 +225,13 @@ export function EditorActionBar() {
title={tool.title}
key={tool.title}
sx={{
height: "100%",
p: 1,
alignItems: "center",
bg: "transparent",
display: [
"hideOnMobile" in tool && tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
],
borderRadius: 0,
flexShrink: 0,
"&:hover svg path": {
fill:
@@ -267,7 +242,7 @@ export function EditorActionBar() {
}}
onClick={tool.onClick}
>
<tool.icon size={18} />
<tool.icon size={16} />
</Button>
))}
</Flex>
@@ -275,183 +250,209 @@ export function EditorActionBar() {
);
}
function TabStrip() {
const sessions = useEditorStore((store) => store.sessions);
const activeSessionId = useEditorStore((store) => store.activeSessionId);
const TabStrip = React.memo(function TabStrip() {
useEditorStore((store) => store.getActiveSession()); // otherwise the tab title won't update on opening a note
const tabs = useEditorStore((store) => store.tabs);
const currentTab = useEditorStore((store) => store.activeTabId);
const canGoBack = useEditorStore((store) => store.canGoBack);
const canGoForward = useEditorStore((store) => store.canGoForward);
return (
<ScrollContainer
className="tabsScroll"
suppressScrollY
style={{ flex: 1, height: TITLE_BAR_HEIGHT }}
trackStyle={() => ({
backgroundColor: "transparent",
"--ms-track-size": "6px"
})}
thumbStyle={() => ({ height: 3 })}
onWheel={(e) => {
const scrollcontainer = document.querySelector(".tabsScroll");
if (!scrollcontainer) return;
if (e.deltaY > 0) scrollcontainer.scrollLeft += 100;
else if (e.deltaY < 0) scrollcontainer.scrollLeft -= 100;
}}
>
<Flex sx={{ flex: 1 }}>
<Flex
sx={{
flex: 1,
height: "100%"
px: 1,
borderRight: "1px solid var(--border)",
alignItems: "center",
flexShrink: 0
}}
onDoubleClick={async (e) => {
e.stopPropagation();
useEditorStore.getState().newSession();
}}
data-test-id="tabs"
onDoubleClick={(e) => e.stopPropagation()}
>
<ReorderableList
items={sessions}
moveItem={(from, to) => {
if (from === to) return;
const sessions = useEditorStore.getState().sessions.slice();
const isToPinned = sessions[to].pinned;
const [fromTab] = sessions.splice(from, 1);
// if the tab where this tab is being dropped is pinned,
// let's pin our tab too.
if (isToPinned) {
fromTab.pinned = true;
fromTab.preview = false;
}
// unpin the tab if it is moved.
else if (fromTab.pinned) fromTab.pinned = false;
sessions.splice(to, 0, fromTab);
useEditorStore.setState({ sessions });
<Button
disabled={!canGoBack}
onClick={() => useEditorStore.getState().goBack()}
variant="secondary"
sx={{ p: 1, bg: "transparent" }}
data-test-id="go-back"
>
<ArrowLeft size={16} />
</Button>
<Button
disabled={!canGoForward}
onClick={() => useEditorStore.getState().goForward()}
variant="secondary"
sx={{ p: 1, bg: "transparent" }}
data-test-id="go-forward"
>
<ArrowRight size={16} />
</Button>
</Flex>
<ScrollContainer
className="tabsScroll"
suppressScrollY
style={{ flex: 1, height: TITLE_BAR_HEIGHT }}
trackStyle={() => ({
backgroundColor: "transparent",
"--ms-track-size": "6px"
})}
thumbStyle={() => ({ height: 3 })}
onWheel={(e) => {
const scrollcontainer = document.querySelector(".tabsScroll");
if (!scrollcontainer) return;
if (e.deltaY > 0) scrollcontainer.scrollLeft += 100;
else if (e.deltaY < 0) scrollcontainer.scrollLeft -= 100;
}}
>
<Flex
sx={{
flex: 1,
height: "100%"
}}
renderItem={({ item: session, index: i }) => {
const isUnsaved =
session.type === "default" &&
session.saveState === SaveState.NotSaved;
return (
<Tab
id={session.id}
key={session.id}
title={
session.title ||
("note" in session ? session.note.title : "Untitled")
}
isUnsaved={isUnsaved}
isTemporary={!!session.preview}
isActive={session.id === activeSessionId}
isPinned={!!session.pinned}
isLocked={isLockedSession(session)}
type={session.type}
onKeepOpen={() =>
useEditorStore
.getState()
.updateSession(
session.id,
[session.type],
(s) => (s.preview = false)
)
}
onFocus={() => {
if (session.id !== activeSessionId) {
useEditorStore.getState().openSession(session.id);
onDoubleClick={async (e) => {
e.stopPropagation();
useEditorStore.getState().addTab();
}}
data-test-id="tabs"
>
<ReorderableList
items={tabs}
moveItem={(from, to) => {
if (from === to) return;
const tabs = useEditorStore.getState().tabs.slice();
const isToPinned = tabs[to].pinned;
const [fromTab] = tabs.splice(from, 1);
// if the tab where this tab is being dropped is pinned,
// let's pin our tab too.
if (isToPinned) {
fromTab.pinned = true;
}
// unpin the tab if it is moved.
else if (fromTab.pinned) fromTab.pinned = false;
tabs.splice(to, 0, fromTab);
useEditorStore.setState({ tabs });
}}
renderItem={({ item: tab, index: i }) => {
const session = useEditorStore
.getState()
.getSession(tab.sessionId);
if (!session) return null;
const isUnsaved =
session.type === "default" &&
session.saveState === SaveState.NotSaved;
return (
<Tab
id={tab.id}
key={tab.sessionId}
title={
session.title ||
("note" in session
? session.note.title
: strings.untitled())
}
}}
onClose={() =>
useEditorStore.getState().closeSessions(session.id)
}
onCloseAll={() =>
useEditorStore
.getState()
.closeSessions(
...sessions.filter((s) => !s.pinned).map((s) => s.id)
)
}
onCloseOthers={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s) => s.id !== session.id && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheRight={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s, index) => index > i && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheLeft={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s, index) => index < i && !s.pinned)
.map((s) => s.id)
)
}
onRevealInList={() =>
AppEventManager.publish(
AppEvents.revealItemInList,
"note" in session ? session.note.id : session.id,
true
)
}
onPin={() => {
useEditorStore.setState((state) => {
// preview tabs can never be pinned.
if (!session.pinned) state.sessions[i].preview = false;
state.sessions[i].pinned = !session.pinned;
state.sessions.sort((a, b) =>
isUnsaved={isUnsaved}
isActive={tab.id === currentTab}
isPinned={!!tab.pinned}
isLocked={isLockedSession(session)}
type={session.type}
onFocus={() => {
if (tab.id !== currentTab) {
useEditorStore.getState().activateSession(tab.sessionId);
}
}}
onClose={() => useEditorStore.getState().closeTabs(tab.id)}
onCloseAll={() =>
useEditorStore
.getState()
.closeTabs(
...tabs.filter((s) => !s.pinned).map((s) => s.id)
)
}
onCloseOthers={() =>
useEditorStore
.getState()
.closeTabs(
...tabs
.filter((s) => s.id !== tab.id && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheRight={() =>
useEditorStore
.getState()
.closeTabs(
...tabs
.filter((s, index) => index > i && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheLeft={() =>
useEditorStore
.getState()
.closeTabs(
...tabs
.filter((s, index) => index < i && !s.pinned)
.map((s) => s.id)
)
}
onRevealInList={
"note" in session
? () =>
AppEventManager.publish(
AppEvents.revealItemInList,
session.note.id,
true
)
: undefined
}
onPin={() => {
const tabs = useEditorStore.getState().tabs.slice();
const index = tabs.findIndex((t) => t.id === tab.id);
tabs[index].pinned = !tabs[index].pinned;
tabs.sort((a, b) =>
a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1
);
});
}}
/>
);
}}
/>
</Flex>
</ScrollContainer>
useEditorStore.setState({ tabs });
}}
/>
);
}}
/>
</Flex>
</ScrollContainer>
</Flex>
);
}
});
type TabProps = {
id: string;
title: string;
isActive: boolean;
isTemporary: boolean;
isPinned: boolean;
isLocked: boolean;
isUnsaved: boolean;
type: SessionType;
onKeepOpen: () => void;
onFocus: () => void;
onClose: () => void;
onCloseOthers: () => void;
onCloseToTheRight: () => void;
onCloseToTheLeft: () => void;
onCloseAll: () => void;
onRevealInList: () => void;
onPin: () => void;
onRevealInList?: () => void;
};
function Tab(props: TabProps) {
const {
id,
title,
isActive,
isTemporary,
isPinned,
isLocked,
isUnsaved,
type,
onKeepOpen,
onFocus,
onClose,
onCloseAll,
@@ -493,13 +494,14 @@ function Tab(props: TabProps) {
setNodeRef(el);
activeTabRef.current = el;
}}
className="tab"
className={`tab${isActive || active?.id === id ? " active" : ""}`}
data-test-id={`tab-${id}`}
sx={{
height: "100%",
cursor: "pointer",
px: 2,
pl: 2,
borderRight: "1px solid var(--border)",
":last-of-type": { borderRight: 0 },
transform: CSS.Transform.toString(transform),
transition,
@@ -511,7 +513,7 @@ function Tab(props: TabProps) {
flexShrink: 0,
":hover": {
"& .closeTabButton": {
visibility: "visible"
opacity: 1
},
bg: isActive ? "hover-selected" : "hover"
}
@@ -554,16 +556,10 @@ function Tab(props: TabProps) {
type: "button",
title: strings.revealInList(),
key: "reveal-in-list",
onClick: onRevealInList
},
{ type: "separator", key: "sep" },
{
type: "button",
key: "keep-open",
title: strings.keepOpen(),
onClick: onKeepOpen,
isDisabled: !isTemporary
onClick: onRevealInList,
isHidden: !onRevealInList
},
{ type: "separator", key: "sep2" },
{
type: "button",
key: "pin",
@@ -574,37 +570,29 @@ function Tab(props: TabProps) {
}
]);
}}
onDoubleClick={(e) => {
e.stopPropagation();
if (isTemporary) onKeepOpen();
}}
onAuxClick={(e) => {
if (e.button == 1) onClose();
}}
onClick={() => onFocus()}
{...listeners}
{...attributes}
>
<Flex
mr={1}
onMouseUp={(e) => {
if (e.button == 0) onFocus();
}}
>
<Flex>
<Icon
data-test-id={`tab-icon${isUnsaved ? "-unsaved" : ""}`}
size={16}
size={14}
color={
isUnsaved ? "accent-error" : isActive ? "accent-selected" : "icon"
}
/>
<Text
data-test-id="tab-title"
variant="body"
sx={{
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflowX: "hidden",
pointerEvents: "none",
fontStyle: isTemporary ? "italic" : "normal",
maxWidth: 120,
color: isActive ? "paragraph-selected" : "paragraph"
}}
@@ -616,34 +604,33 @@ function Tab(props: TabProps) {
{isPinned ? (
<Pin
sx={{
":hover": { bg: "border" },
borderRadius: "default",
flexShrink: 0
}}
size={14}
onMouseUp={(e) => {
if (e.button == 0) {
e.stopPropagation();
onPin();
flexShrink: 0,
ml: "small",
mr: 1,
"&:hover": {
bg: "hover-secondary"
}
}}
size={14}
onClick={onPin}
/>
) : (
<Cross
sx={{
visibility: isActive && active?.id !== id ? "visible" : "hidden",
":hover": { bg: "border" },
borderRadius: "default",
flexShrink: 0
}}
onMouseUp={(e) => {
if (e.button == 0) {
onClose();
flexShrink: 0,
opacity: isActive || active?.id === id ? 1 : 0,
ml: "small",
mr: 1,
"&:hover": {
bg: "hover-secondary"
}
}}
onClick={onClose}
className="closeTabButton"
data-test-id={"tab-close-button"}
size={16}
size={14}
/>
)}
</Flex>
@@ -652,7 +639,7 @@ function Tab(props: TabProps) {
type ReorderableListProps<T> = {
items: T[];
renderItem: (props: { item: T; index: number }) => JSX.Element;
renderItem: (props: { item: T; index: number }) => JSX.Element | null;
moveItem: (from: number, to: number) => void;
};
@@ -714,12 +701,3 @@ function ReorderableList<T extends { id: string }>(
</DndContext>
);
}
function enterFullscreen(elem: HTMLElement) {
elem.requestFullscreen();
}
function exitFullscreen() {
if (!document.fullscreenElement) return;
document.exitFullscreen();
}

View File

@@ -19,12 +19,31 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Button, Flex, Text } from "@theme-ui/components";
import { SaveState, useEditorStore } from "../../stores/editor-store";
import { Loading, Saved, NotSaved } from "../icons";
import { useEditorConfig, useNoteStatistics } from "./manager";
import { useStore as useAppStore } from "../../stores/app-store";
import {
Loading,
Saved,
NotSaved,
FocusMode,
Plus,
Minus,
EditorNormalWidth,
TableOfContents,
ExitFullscreen,
Fullscreen,
EditorFullWidth,
NormalMode
} from "../icons";
import {
useEditorConfig,
useNoteStatistics,
useEditorManager
} from "./manager";
import { getFormattedDate } from "@notesnook/common";
import { MAX_AUTO_SAVEABLE_WORDS } from "./types";
import { strings } from "@notesnook/intl";
import { EDITOR_ZOOM } from "./common";
import { useWindowControls } from "../../hooks/use-window-controls";
const SAVE_STATE_ICON_MAP = {
"-1": NotSaved,
@@ -33,15 +52,59 @@ const SAVE_STATE_ICON_MAP = {
};
function EditorFooter() {
const { isFullscreen } = useWindowControls();
const { words } = useNoteStatistics();
const session = useEditorStore((store) => store.getActiveSession());
const { editorConfig, setEditorConfig } = useEditorConfig();
const editorMargins = useEditorStore((store) => store.editorMargins);
const isFocusMode = useAppStore((store) => store.isFocusMode);
if (!session) return null;
const saveState =
session.type === "default" ? session.saveState : SaveState.NotSaved;
const dateEdited = "note" in session ? session.note.dateEdited : 0;
const SaveStateIcon = SAVE_STATE_ICON_MAP[saveState];
const tools = [
{
title: editorMargins
? strings.disableEditorMargins()
: strings.enableEditorMargins(),
icon: editorMargins ? EditorNormalWidth : EditorFullWidth,
enabled: true,
hideOnMobile: true,
onClick: () => useEditorStore.getState().toggleEditorMargins()
},
{
title: isFullscreen
? strings.exitFullScreen()
: strings.enterFullScreen(),
icon: isFullscreen ? ExitFullscreen : Fullscreen,
enabled: true,
hidden: !isFocusMode,
hideOnMobile: true,
onClick: () => {
if (isFullscreen) {
exitFullscreen();
} else {
enterFullscreen(document.documentElement);
}
}
},
{
title: isFocusMode ? strings.normalMode() : strings.focusMode(),
icon: isFocusMode ? FocusMode : NormalMode,
enabled: true,
hideOnMobile: true,
onClick: () => {
useAppStore.getState().toggleFocusMode();
if (document.fullscreenElement) exitFullscreen();
const editor =
session && useEditorManager.getState().getEditor(session.id);
if (editor) editor.editor?.focus();
}
}
];
return (
<Flex sx={{ alignItems: "center", justifyContent: "center", gap: 2 }}>
@@ -49,9 +112,28 @@ function EditorFooter() {
sx={{
alignItems: "center",
justifyContent: "center",
flex: 1
flex: 1,
height: "100%"
}}
>
{tools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
title={tool.title}
key={tool.title}
onClick={tool.onClick}
sx={{
py: 0,
px: 1,
height: "100%",
display: tool.hidden ? "none" : "block"
}}
variant="icon"
>
<tool.icon size={13} />
</Button>
))}
<Button
variant="icon"
onClick={() =>
@@ -63,15 +145,11 @@ function EditorFooter() {
})
}
disabled={editorConfig.zoom <= EDITOR_ZOOM.MIN}
sx={{ py: 0, height: "100%" }}
sx={{ py: 0, px: 1, height: "100%" }}
>
<b>-</b>
<Minus size={13} />
</Button>
<Text
className="selectable"
variant="subBody"
sx={{ color: "paragraph" }}
>
<Text variant="subBody" sx={{ color: "paragraph" }}>
{editorConfig.zoom}%
</Text>
<Button
@@ -85,9 +163,9 @@ function EditorFooter() {
})
}
disabled={editorConfig.zoom >= EDITOR_ZOOM.MAX}
sx={{ py: 0, height: "100%" }}
sx={{ py: 0, px: 1, height: "100%" }}
>
<b>+</b>
<Plus size={13} />
</Button>
</Flex>
{words.total > MAX_AUTO_SAVEABLE_WORDS ? (
@@ -142,3 +220,12 @@ function EditorFooter() {
);
}
export default EditorFooter;
function enterFullscreen(elem: HTMLElement) {
elem.requestFullscreen();
}
function exitFullscreen() {
if (!document.fullscreenElement) return;
document.exitFullscreen();
}

View File

@@ -76,7 +76,7 @@ import { TITLE_BAR_HEIGHT } from "../title-bar";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
const autoSaveToast = { show: true, hide: () => { } };
const autoSaveToast = { show: true, hide: () => {} };
async function saveContent(
noteId: string,
@@ -116,40 +116,15 @@ async function saveContent(
const deferredSave = debounceWithId(saveContent, 100);
export default function TabsView() {
const sessions = useEditorStore((store) => store.sessions);
const tabs = useEditorStore((store) => store.tabs);
const documentPreview = useEditorStore((store) => store.documentPreview);
const activeSessionId = useEditorStore((store) => store.activeSessionId);
const activeTab = useEditorStore((store) => store.getActiveTab());
const arePropertiesVisible = useEditorStore(
(store) => store.arePropertiesVisible
);
const isTOCVisible = useEditorStore((store) => store.isTOCVisible);
const [dropRef, overlayRef] = useDragOverlay();
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (
(event.ctrlKey || event.metaKey) &&
event.altKey &&
event.key === "ArrowRight"
) {
event.preventDefault();
useEditorStore.getState().openNextSession();
}
if (
(event.ctrlKey || event.metaKey) &&
event.altKey &&
event.key === "ArrowLeft"
) {
event.preventDefault();
useEditorStore.getState().openPreviousSession();
}
};
document.body.addEventListener("keydown", onKeyDown);
return () => {
document.body.removeEventListener("keydown", onKeyDown);
};
}, []);
return (
<>
<Flex
@@ -176,17 +151,24 @@ export default function TabsView() {
>
<SplitPane direction="vertical" autoSaveId={"editor-panels"}>
<Pane id="editor-panel" className="editor-pane">
{sessions.map((session) => (
<Freeze key={session.id} freeze={session.id !== activeSessionId}>
{session.type === "locked" ? (
<UnlockNoteView session={session} />
) : session.type === "conflicted" || session.type === "diff" ? (
<DiffViewer session={session} />
) : (
<MemoizedEditorView session={session} />
)}
</Freeze>
))}
{tabs.map((tab) => {
const session = useEditorStore
.getState()
.getSession(tab.sessionId);
if (!session) return null;
return (
<Freeze key={session.id} freeze={tab.id !== activeTab?.id}>
{session.type === "locked" ? (
<UnlockNoteView session={session} />
) : session.type === "conflicted" ||
session.type === "diff" ? (
<DiffViewer session={session} />
) : (
<MemoizedEditorView session={session} />
)}
</Freeze>
);
})}
</Pane>
{documentPreview ? (
@@ -226,15 +208,15 @@ export default function TabsView() {
</Pane>
) : null}
{isTOCVisible && activeSessionId ? (
{isTOCVisible && activeTab ? (
<Pane id="table-of-contents-pane" initialSize={300} minSize={300}>
<TableOfContents sessionId={activeSessionId} />
<TableOfContents sessionId={activeTab.sessionId} />
</Pane>
) : null}
</SplitPane>
<DropZone overlayRef={overlayRef} />
{arePropertiesVisible && activeSessionId && (
<Properties sessionId={activeSessionId} />
{arePropertiesVisible && activeTab && (
<Properties sessionId={activeTab.sessionId} />
)}
</ScopedThemeProvider>
</>
@@ -252,10 +234,10 @@ function EditorView({
session
}: {
session:
| DefaultEditorSession
| NewEditorSession
| ReadonlyEditorSession
| DeletedEditorSession;
| DefaultEditorSession
| NewEditorSession
| ReadonlyEditorSession
| DeletedEditorSession;
}) {
const lastChangedTime = useRef<number>(0);
const root = useRef<HTMLDivElement>(null);
@@ -294,6 +276,7 @@ function EditorView({
editor.updateContent(result.data);
} else if (isNote && session.note.title !== item.title) {
AppEventManager.publish(AppEvents.changeNoteTitle, {
sessionId: session.id,
title: item.title,
preventSave: true
});
@@ -319,7 +302,7 @@ function EditorView({
if (!session.needsHydration && session.content) {
editor?.updateContent(session.content.data);
}
}, [editor, session.needsHydration]);
}, [editor, session]);
return (
<Flex
@@ -347,19 +330,39 @@ function EditorView({
onSave={(content, ignoreEdit) => {
const currentSession = useEditorStore
.getState()
.getSession(session.id, ["default", "readonly", "new"]);
if (!currentSession) return;
.getSession(session.id);
const noteId =
currentSession && "note" in currentSession
? currentSession.note.id
: null;
const sessions = noteId
? useEditorStore.getState().getSessionsForNote(noteId)
: [currentSession];
const currentSessionId = session.id;
const data = content();
if (!currentSession.content)
currentSession.content = { type: "tiptap", data };
else currentSession.content.data = data;
for (const session of sessions) {
if (
session?.type !== "default" &&
session?.type !== "readonly" &&
session?.type !== "new"
)
continue;
if (!session.content) session.content = { type: "tiptap", data };
else session.content.data = data;
// update content in other tabs
if (session.id !== currentSessionId) {
const editor = useEditorManager.getState().getEditor(session.id);
editor?.editor?.updateContent(data);
}
}
logger.debug("scheduling save", {
id: session.id,
length: data.length
});
deferredSave(currentSession.id, currentSession.id, ignoreEdit, data);
deferredSave(session.id, session.id, ignoreEdit, data);
}}
options={{
readonly: session?.type === "readonly" || session?.type === "deleted",
@@ -414,9 +417,9 @@ function DownloadAttachmentProgress(props: DownloadAttachmentProgressProps) {
variant="secondary"
mt={2}
onClick={() => {
const id = useEditorStore.getState().activeSessionId;
const note = useEditorStore.getState().getActiveNote();
useEditorStore.setState({ documentPreview: undefined });
if (id) db.fs().cancel(id).catch(console.error);
if (note) db.fs().cancel(note.id).catch(console.error);
}}
>
{strings.cancel()}
@@ -493,6 +496,14 @@ export function Editor(props: EditorProps) {
return () => unsub();
}, []);
useEffect(() => {
return () => {
const editor = useEditorManager.getState().getEditor(id)?.editor;
const selection = editor?.getSelection();
if (selection) Config.set(`${id}:selection`, selection);
};
}, [id]);
return (
<EditorChrome {...props}>
<Tiptap
@@ -884,7 +895,7 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
subtitle={strings.enterPasswordToUnlockNote()}
title={session.note.title}
unlock={async (password) => {
const note = await db.vault.open(session.id, password);
const note = await db.vault.open(session.note.id, password);
if (!note || !note.content)
throw new Error("note with this id does not exist.");
@@ -897,8 +908,7 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
saveState: SaveState.Saved,
sessionId: `${Date.now()}`,
tags,
pinned: session.pinned,
preview: session.preview,
tabId: session.tabId,
content: note.content
});
}}

View File

@@ -604,7 +604,11 @@ function toIEditor(editor: Editor): IEditor {
),
startSearch: () => editor.commands.startSearch(),
getContent: () =>
getHTMLFromFragment(editor.state.doc.content, editor.schema)
getHTMLFromFragment(editor.state.doc.content, editor.schema),
getSelection: () => {
const { from, to } = editor.state.selection;
return { from, to };
}
};
}

View File

@@ -17,7 +17,14 @@ 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 React, { useCallback, useEffect, useMemo, useRef } from "react";
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
} from "react";
import { Input } from "@theme-ui/components";
import { useEditorStore } from "../../stores/editor-store";
import { debounceWithId } from "@notesnook/common";
@@ -40,7 +47,7 @@ function TitleBox(props: TitleBoxProps) {
const inputRef = useRef<HTMLInputElement>(null);
const pendingChanges = useRef(false);
// const id = useStore((store) => store.session.id);
const sessionType = useEditorStore((store) => store.getActiveSession()?.type);
const sessionType = useEditorStore((store) => store.getSession(id)?.type);
const isMobile = useMobile();
const isTablet = useTablet();
const { editorConfig } = useEditorConfig();
@@ -64,13 +71,15 @@ function TitleBox(props: TitleBoxProps) {
[isMobile, isTablet]
);
useEffect(() => {
useLayoutEffect(() => {
const session = useEditorStore.getState().getSession(id);
if (!session || !("note" in session) || !session.note || !inputRef.current)
return;
if (pendingChanges.current) return;
const { title } = session.note;
if (inputRef.current.value === title) return;
withSelectionPersist(
inputRef.current,
(input) => (input.value = title || "")
@@ -86,23 +95,24 @@ function TitleBox(props: TitleBoxProps) {
useEffect(() => {
const { unsubscribe } = AppEventManager.subscribe(
AppEvents.changeNoteTitle,
({ preventSave, title }: { title: string; preventSave: boolean }) => {
if (!inputRef.current) return;
({
preventSave,
title,
sessionId
}: {
title: string;
preventSave: boolean;
sessionId: string;
}) => {
if (!inputRef.current || sessionId !== id) return;
withSelectionPersist(
inputRef.current,
(input) => (input.value = title)
);
updateFontSize(title.length);
if (!preventSave) {
const { activeSessionId } = useEditorStore.getState();
if (!activeSessionId) return;
pendingChanges.current = true;
debouncedOnTitleChange(
activeSessionId,
activeSessionId,
title,
pendingChanges
);
debouncedOnTitleChange(sessionId, sessionId, title, pendingChanges);
}
}
);
@@ -110,7 +120,7 @@ function TitleBox(props: TitleBoxProps) {
return () => {
unsubscribe();
};
}, [updateFontSize]);
}, [updateFontSize, id]);
return (
<Input

View File

@@ -40,4 +40,5 @@ export interface IEditor {
sendAttachmentProgress: (hash: string, progress: number) => void;
startSearch: () => void;
getContent: () => string;
getSelection: () => { from: number; to: number };
}

View File

@@ -219,7 +219,8 @@ import {
mdiOpenInNew,
mdiTagOutline,
mdiChatQuestionOutline,
mdiNoteRemoveOutline
mdiNoteRemoveOutline,
mdiTabPlus
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -273,7 +274,8 @@ const MDIIconWrapper = memo(
(prev, next) =>
prev.rotate === next.rotate &&
prev.color === next.color &&
prev.title === next.title
prev.title === next.title &&
prev.size === next.size
);
export type IconProps = FlexProps & Omit<MDIIconWrapperProps, "path">;
@@ -560,3 +562,4 @@ export const ClearCache = createIcon(mdiBroom);
export const OpenInNew = createIcon(mdiOpenInNew);
export const Coupon = createIcon(mdiTagOutline);
export const Support = createIcon(mdiChatQuestionOutline);
export const NewTab = createIcon(mdiTabPlus);

View File

@@ -118,7 +118,7 @@ function Note(props: NoteProps) {
} = props;
const note = item;
const isOpened = useEditorStore((store) => store.activeSessionId === item.id);
const isOpened = useEditorStore((store) => store.isNoteOpen(item.id));
const primary: SchemeColors = color ? color.colorCode : "accent-selected";
return (
@@ -146,7 +146,7 @@ function Note(props: NoteProps) {
menuItems={menuItems}
onClick={() => useEditorStore.getState().openSession(note)}
onMiddleClick={() =>
useEditorStore.getState().openSession(note, { newSession: true })
useEditorStore.getState().openSession(note, { openInNewTab: true })
}
header={
<Flex
@@ -331,6 +331,14 @@ const menuItems: (
// const isSynced = db.notes.note(note.id)?.synced();
return [
{
type: "button",
key: "openinnewtab",
title: strings.openInNewTab(),
icon: OpenInNew.path,
onClick: () =>
useEditorStore.getState().openSession(note.id, { openInNewTab: true })
},
{
type: "button",
key: "pin",

View File

@@ -140,7 +140,7 @@ function Header(props: RouteContainerProps) {
{buttons?.back ? (
<Button
{...buttons.back}
data-test-id="go-back"
data-test-id="route-go-back"
sx={{ p: 0, flexShrink: 0 }}
>
<ArrowLeft size={20} />

View File

@@ -59,6 +59,7 @@ type PaneOptions = {
export type SplitPaneImperativeHandle = {
collapse: (index: number) => void;
expand: (index: number) => void;
isCollapsed: (index: number) => boolean;
};
export const SplitPane = React.forwardRef<
SplitPaneImperativeHandle,
@@ -96,14 +97,15 @@ export const SplitPane = React.forwardRef<
const wrapSize = useRef(0);
const childrenLength = childrenToArray(children).length;
const autoSaveKey = autoSaveId ? `csp:${autoSaveId}` : undefined;
const lastCollapsedPaneSize = useRef(0);
const { sizeName, splitPos, splitAxis } = useMemo(
() =>
({
sizeName: direction === "vertical" ? "width" : "height",
splitPos: direction === "vertical" ? "left" : "top",
splitAxis: direction === "vertical" ? "x" : "y"
} as const),
({
sizeName: direction === "vertical" ? "width" : "height",
splitPos: direction === "vertical" ? "left" : "top",
splitAxis: direction === "vertical" ? "x" : "y"
} as const),
[direction]
);
@@ -164,7 +166,7 @@ export const SplitPane = React.forwardRef<
if (wrapSize.current === 0) return;
updatePaneLimitSizes(children);
setSizes(paneSizes.current, wrapSize.current, false);
setSizes(paneSizes.current, wrapSize.current, true);
}, [children, childrenLength]);
const setSizes = useCallback(
@@ -256,11 +258,18 @@ export const SplitPane = React.forwardRef<
return {
collapse: (index: number) => {
paneSizes.current[index].collapsed = true;
lastCollapsedPaneSize.current = paneSizes.current[index].size;
setSizes(paneSizes.current, wrapSize.current);
},
expand: (index: number) => {
paneSizes.current[index].collapsed = false;
paneSizes.current[index].size = lastCollapsedPaneSize.current
? lastCollapsedPaneSize.current
: paneSizes.current[index].initialSize;
setSizes(paneSizes.current, wrapSize.current);
},
isCollapsed: (index: number) => {
return paneSizes.current[index].collapsed;
}
};
},

View File

@@ -62,7 +62,7 @@ function StatusBar() {
borderTop: "1px solid",
borderTopColor: "separator",
justifyContent: "space-between",
display: ["none", "none", "flex"],
display: ["none", "flex", "flex"],
flexShrink: 0,
height: 24
}}

View File

@@ -34,7 +34,7 @@ import { strings } from "@notesnook/intl";
type TrashItemProps = { item: TrashItemType; date: number };
function TrashItem(props: TrashItemProps) {
const { item, date } = props;
const isOpened = useEditorStore((store) => store.activeSessionId === item.id);
const isOpened = useEditorStore((store) => store.isNoteOpen(item.id));
return (
<ListItem

View File

@@ -93,14 +93,9 @@ const features: Record<FeatureKeys, Feature> = {
]
: [
{
title: "Notesnook Gift Cards",
title: "Better tabs",
subtitle:
"You can now gift Notesnook Pro to your friends and family. Gift cards are available in 1, 3, and 5-year plans at https://notesnook.com/giftcards."
},
{
title: "Sorted search results",
subtitle:
"Search results are now sorted by date created by default."
"Tabs have been overhauled to be more predictable and easier to use. Give it a try! We are sure you'll love it."
}
],
cta: {

View File

@@ -187,7 +187,7 @@ const SettingsGroups = [
// 2. Settings will be synced so their state must be serializable
// 3. Settings will be grouped
// - Where group header is customizable
// 4. Sections/groups must be able to accomodate tips & tutorials for future.
// 4. Sections/groups must be able to accommodate tips & tutorials for future.
// 5. Settings will be stateful but independent such that any one setting
// can appear independent of others (e.g. as a search result)

View File

@@ -39,11 +39,14 @@ import {
isTrashItem,
NoteContent
} from "@notesnook/core";
import { Context } from "../components/list-container/types";
import { showToast } from "../utils/toast";
import { getId } from "@notesnook/core";
import { PersistStorage } from "zustand/middleware";
import { getFormattedHistorySessionDate } from "@notesnook/common";
import {
getFormattedHistorySessionDate,
TabHistory,
TabSessionHistory
} from "@notesnook/common";
import { isCipher } from "@notesnook/core";
import { hashNavigate } from "../navigation";
import { AppEventManager, AppEvents } from "../common/app-events";
@@ -64,15 +67,17 @@ enum SESSION_STATES {
conflicted
}
// type ConflictedContentItem = Omit<ContentItem, "conflicted"> & {
// conflicted: ContentItem;
// };
type TabItem = {
id: string;
sessionId: string;
pinned?: boolean;
};
export type BaseEditorSession = {
tabId: string;
id: string;
needsHydration?: boolean;
pinned?: boolean;
preview?: boolean;
title?: string;
/**
@@ -115,22 +120,29 @@ export type DefaultEditorSession = BaseEditorSession & {
export type NewEditorSession = BaseEditorSession & {
type: "new";
context?: Context;
saveState: SaveState;
content?: NoteContent<false>;
};
export type ConflictedEditorSession = BaseEditorSession & {
type: "conflicted" | "diff";
type: "conflicted";
note: Note;
content?: ContentItem;
};
export type DiffEditorSession = BaseEditorSession & {
type: "diff";
note: Note;
content: ContentItem;
historySessionId: string;
};
export type EditorSession =
| DefaultEditorSession
| LockedEditorSession
| NewEditorSession
| ConflictedEditorSession
| DiffEditorSession
| ReadonlyEditorSession
| DeletedEditorSession;
@@ -140,7 +152,7 @@ type SessionTypeMap = {
locked: LockedEditorSession;
new: NewEditorSession;
conflicted: ConflictedEditorSession;
diff: ConflictedEditorSession;
diff: DiffEditorSession;
readonly: ReadonlyEditorSession;
deleted: DeletedEditorSession;
};
@@ -160,10 +172,33 @@ export function isLockedSession(session: EditorSession): boolean {
session.content.locked)
);
}
const tabSessionHistory = new TabSessionHistory({
get() {
return {
tabSessionHistory: useEditorStore.getState().tabHistory,
canGoBack: useEditorStore.getState().canGoBack,
canGoForward: useEditorStore.getState().canGoForward
};
},
set(state) {
useEditorStore.setState({
tabHistory: state.tabSessionHistory,
canGoBack: state.canGoBack,
canGoForward: state.canGoForward
});
}
});
const saveMutex = new Mutex();
class EditorStore extends BaseStore<EditorStore> {
tabs: TabItem[] = [];
tabHistory: TabHistory = {};
activeTabId: string | undefined;
canGoBack = false;
canGoForward = false;
sessions: EditorSession[] = [];
activeSessionId?: string;
arePropertiesVisible = false;
documentPreview?: DocumentPreview;
@@ -178,18 +213,25 @@ class EditorStore extends BaseStore<EditorStore> {
);
};
getActiveSession = <T extends SessionType[]>(types?: T) => {
const { activeSessionId, sessions } = this.get();
return sessions.find(
(s): s is SessionTypeMap[T[number]] =>
s.id === activeSessionId && (!types || types.includes(s.type))
getSessionsForNote = (noteId: string) => {
return this.get().sessions.filter(
(s) => "note" in s && s.note.id === noteId
);
};
getTabsForNote = (noteId: string) => {
const { tabs, sessions } = this.get();
return tabs.filter((t) =>
sessions.some(
(s) => t.sessionId === s.id && "note" in s && s.note.id === noteId
)
);
};
init = () => {
EV.subscribe(EVENTS.userLoggedOut, () => {
const { closeSessions, sessions } = this.get();
closeSessions(...sessions.map((s) => s.id));
const { closeTabs, tabs } = this.get();
closeTabs(...tabs.map((s) => s.id));
});
EV.subscribe(EVENTS.vaultLocked, () => {
@@ -199,8 +241,7 @@ class EditorStore extends BaseStore<EditorStore> {
if (
session.type === "diff" ||
session.type === "deleted" ||
// TODO: what's this?
!("note" in session)
session.type === "new"
)
return session;
@@ -208,8 +249,7 @@ class EditorStore extends BaseStore<EditorStore> {
type: "locked",
id: session.id,
note: session.note,
pinned: session.pinned,
preview: session.preview
tabId: session.tabId
};
}
return session;
@@ -221,8 +261,7 @@ class EditorStore extends BaseStore<EditorStore> {
EVENTS.syncItemMerged,
(item?: MaybeDeletedItem<Item>) => {
if (!item) return;
const { sessions, closeSessions, updateSession, openSession } =
this.get();
const { sessions, closeTabs, updateSession, openSession } = this.get();
const clearIds: string[] = [];
for (const session of sessions) {
if (session.type === "new") continue;
@@ -233,9 +272,9 @@ class EditorStore extends BaseStore<EditorStore> {
: item.type === "tiptap"
? item.noteId
: null;
if (noteId && session.id !== noteId && session.note.id !== noteId)
continue;
if (isDeleted(item) || isTrashItem(item)) clearIds.push(session.id);
if (noteId && session.note.id !== noteId) continue;
if (isDeleted(item) || isTrashItem(item))
clearIds.push(session.tabId);
// if a note becomes conflicted, reopen the session
else if (
session.type !== "conflicted" &&
@@ -287,33 +326,31 @@ class EditorStore extends BaseStore<EditorStore> {
);
}
}
if (clearIds.length > 0) closeSessions(...clearIds);
if (clearIds.length > 0) closeTabs(...clearIds);
}
);
db.eventManager.subscribe(
EVENTS.databaseUpdated,
async (event: DatabaseUpdatedEvent) => {
const { sessions, openSession, closeSessions, updateSession } =
this.get();
const { sessions, openSession, closeTabs, updateSession } = this.get();
const clearIds: string[] = [];
if (event.collection === "notes") {
// when a note is permanently deleted from trash
if (event.type === "softDelete" || event.type === "delete") {
clearIds.push(
...event.ids.filter(
(id) =>
sessions.findIndex(
(s) => s.id === id || ("note" in s && s.note.id === id)
) > -1
)
...sessions
.filter(
(session) =>
"note" in session && event.ids.includes(session.note.id)
)
.map((s) => s.tabId)
);
} else if (event.type === "update") {
for (const session of sessions) {
if (
session.type === "new" ||
(!event.ids.includes(session.id) &&
!event.ids.includes(session.note.id))
!event.ids.includes(session.note.id)
)
continue;
@@ -324,13 +361,13 @@ class EditorStore extends BaseStore<EditorStore> {
// when a note is restored from trash
(session.type === "deleted" && event.item.type !== "trash")
) {
openSession(session.id, { force: true, silent: true });
openSession(session.note.id, { force: true, silent: true });
} else if (
// when a note is moved to trash
session.type !== "deleted" &&
event.item.type === "trash"
) {
clearIds.push(session.id);
clearIds.push(session.tabId);
} else {
updateSession(session.id, [session.type], (session) => {
session.note.pinned =
@@ -365,7 +402,7 @@ class EditorStore extends BaseStore<EditorStore> {
(session.type === "default" && session.locked)) &&
!event.item.locked)
) {
openSession(session.id, { force: true, silent: true });
openSession(session.note.id, { force: true, silent: true });
}
}
}
@@ -394,7 +431,6 @@ class EditorStore extends BaseStore<EditorStore> {
: event.reference.ids;
if (!("color" in session) || !session.color) continue;
if (
!ids.includes(session.id) &&
!ids.includes(session.note.id) &&
!ids.includes(session.color)
)
@@ -415,7 +451,6 @@ class EditorStore extends BaseStore<EditorStore> {
: event.reference.ids;
if (!("tags" in session) || !session.tags) continue;
if (
!ids.includes(session.id) &&
!ids.includes(session.note.id) &&
session.tags.every((t) => !ids.includes(t.id))
)
@@ -444,36 +479,43 @@ class EditorStore extends BaseStore<EditorStore> {
session.tags?.every((t) => !event.ids.includes(t.id))
)
continue;
console.log("UDPATE");
updateSession(session.id, undefined, {
tags: await db.notes.tags(session.note.id)
});
}
}
}
if (clearIds.length > 0) closeSessions(...clearIds);
if (clearIds.length > 0) closeTabs(...clearIds);
}
);
const {
openSession,
openDiffSession,
activateSession,
activeSessionId,
getSession,
newSession
} = this.get();
if (activeSessionId) {
const session = getSession(activeSessionId);
if (!session) return;
if (session.type === "diff" || session.type === "conflicted")
openDiffSession(session.note.id, session.id);
else if (session.type === "new") activateSession(session.id);
else openSession(activeSessionId);
const { rehydrateSession, activeTabId, newSession } = this.get();
if (activeTabId) {
const tab = this.get().tabs.find((t) => t.id === activeTabId);
if (!tab) return;
rehydrateSession(tab.sessionId);
} else newSession();
};
private rehydrateSession = (sessionId: string) => {
const { openSession, openDiffSession, getSession, activateSession } =
this.get();
const session = getSession(sessionId);
if (session?.type === "new") {
activateSession(session.id);
return;
}
if (!session || !session.needsHydration) return;
if (session.type === "diff")
openDiffSession(session.note.id, session.historySessionId);
else
openSession(session.note.id, {
force: true
});
};
updateSession = <T extends SessionType[] = SessionType[]>(
id: string,
types: T | undefined,
@@ -496,14 +538,13 @@ class EditorStore extends BaseStore<EditorStore> {
});
};
activateSession = (id?: string, activeBlockId?: string) => {
activateSession = (id?: string, activeBlockId?: string, silent?: boolean) => {
if (!id) hashNavigate(`/`, { replace: true, notify: false });
const session = this.get().sessions.find((s) => s.id === id);
if (!session) id = undefined;
const activeSession = this.getActiveSession();
if (activeSession) {
this.saveSessionContentIfNotSaved(activeSession.id);
}
@@ -517,13 +558,9 @@ class EditorStore extends BaseStore<EditorStore> {
setDocumentTitle(session.note.title);
} else setDocumentTitle();
this.set({ activeSessionId: id });
AppEventManager.publish(AppEvents.toggleEditor, true);
if (id) {
const { history } = this.get();
if (history.includes(id)) history.splice(history.indexOf(id), 1);
history.push(id);
if (session?.type === "new")
hashNavigate(`/notes/${id}/create`, { replace: true, notify: false });
else hashNavigate(`/notes/${id}/edit`, { replace: true, notify: false });
@@ -533,6 +570,19 @@ class EditorStore extends BaseStore<EditorStore> {
this.updateSession(session.id, [session.type], {
activeBlockId
});
if (session?.tabId) {
const { tabs, activeTabId } = this.get();
const index = tabs.findIndex((t) => t.id === session.tabId);
// no need to focus tab if the same session is already open
if (
index === -1 ||
(activeTabId === session.tabId && tabs[index].sessionId === session.id)
)
return;
this.set((state) => (state.tabs[index].sessionId = session.id));
if (!silent) this.focusTab(session.tabId, session.id);
}
};
openDiffSession = async (noteId: string, sessionId: string) => {
@@ -545,12 +595,30 @@ class EditorStore extends BaseStore<EditorStore> {
if (!oldContent || !currentContent) return;
const { getSession, addSession, sessions, activeTabId, tabs } = this.get();
const tabId = activeTabId ?? this.addTab();
const tab = tabs.find((t) => t.id === tabId);
const activeSession = tab && getSession(tab.sessionId);
const oldSession = sessions.find(
(s) =>
s.type === "diff" &&
s.historySessionId === session.id &&
s.tabId === tabId
);
const tabSessionId =
activeSession?.needsHydration || activeSession?.type === "new"
? activeSession.id
: tabSessionHistory.add(tabId, oldSession?.id);
const label = getFormattedHistorySessionDate(session);
this.get().addSession({
addSession({
type: "diff",
id: session.id,
id: tabSessionId,
note,
tabId,
title: label,
historySessionId: session.id,
content: {
type: oldContent.type,
dateCreated: session.dateCreated,
@@ -573,31 +641,59 @@ class EditorStore extends BaseStore<EditorStore> {
force?: boolean;
activeBlockId?: string;
silent?: boolean;
newSession?: boolean;
openInNewTab?: boolean;
} = {}
): Promise<void> => {
const { getSession, openDiffSession } = this.get();
const {
getSession,
sessions,
tabs,
activateSession,
activeTabId,
rehydrateSession,
getTabsForNote,
addTab
} = this.get();
const noteId = typeof noteOrId === "string" ? noteOrId : noteOrId.id;
const session = getSession(noteId);
const oldTabForNote = options.force ? null : getTabsForNote(noteId).at(0);
const tabId = options.openInNewTab
? addTab(getId())
: oldTabForNote?.id || activeTabId || addTab(getId());
if (session && !options.force) {
if (!session.needsHydration) {
return this.activateSession(noteId, options.activeBlockId);
}
if (session.type === "diff" || session.type === "conflicted") {
return openDiffSession(session.note.id, session.id);
}
const tab = tabs.find((t) => t.id === tabId);
const activeSession = tab && getSession(tab.sessionId);
const noteAlreadyOpened =
activeSession &&
"note" in activeSession &&
activeSession.note.id === noteId &&
// we should allow opening the same note again if a diff session of a note
// is opened in the same tab. This allows for cases where a user opens a diff
// and then wants to open the note in the same tab again.
activeSession.type !== "diff";
if (noteAlreadyOpened && !options.force) {
return activeSession.needsHydration
? rehydrateSession(activeSession.id)
: activateSession(activeSession.id, options.activeBlockId);
}
if (session && session.id) await db.fs().cancel(session.id);
if (activeSession && "note" in activeSession)
await db.fs().cancel(activeSession.note.id);
const note =
typeof noteOrId === "object"
typeof noteOrId === "object" && !activeSession?.needsHydration
? noteOrId
: (await db.notes.note(noteId)) || (await db.notes.trashed(noteId));
if (!note) return;
const isPreview = session ? session.preview : !options?.newSession;
const oldSessionOfNote = sessions.find(
(s) => "note" in s && s.note.id === noteId && s.tabId === tabId
);
const sessionId =
activeSession?.needsHydration ||
activeSession?.type === "new" ||
noteAlreadyOpened
? activeSession.id
: tabSessionHistory.add(tabId, oldSessionOfNote?.id);
const isLocked = await db.vaults.itemExists(note);
if (note.conflicted) {
@@ -626,25 +722,23 @@ class EditorStore extends BaseStore<EditorStore> {
{
type: "conflicted",
content: content,
id: note.id,
pinned: session?.pinned,
id: sessionId,
note,
preview: isPreview,
activeBlockId: options.activeBlockId
activeBlockId: options.activeBlockId,
tabId
},
!options.silent
options.silent
);
} else if (isLocked && note.type !== "trash") {
this.addSession(
{
type: "locked",
id: note.id,
pinned: session?.pinned,
id: sessionId,
note,
preview: isPreview,
activeBlockId: options.activeBlockId
activeBlockId: options.activeBlockId,
tabId
},
!options.silent
options.silent
);
} else {
const content = note.contentId
@@ -661,12 +755,12 @@ class EditorStore extends BaseStore<EditorStore> {
{
type: "deleted",
note,
id: note.id,
pinned: session?.pinned,
id: sessionId,
content,
activeBlockId: options.activeBlockId
activeBlockId: options.activeBlockId,
tabId
},
!options.silent
options.silent
);
} else {
const attachmentsLength = await db.attachments
@@ -679,94 +773,113 @@ class EditorStore extends BaseStore<EditorStore> {
{
type: "readonly",
note,
id: note.id,
pinned: session?.pinned,
id: sessionId,
content,
color: colors[0]?.fromId,
tags,
activeBlockId: options.activeBlockId
activeBlockId: options.activeBlockId,
tabId
},
!options.silent
options.silent
);
} else {
this.addSession(
{
type: "default",
id: note.id,
id: sessionId,
note,
saveState: SaveState.Saved,
sessionId: `${Date.now()}`,
attachmentsLength,
pinned: session?.pinned,
tags,
color: colors[0]?.fromId,
content,
preview: isPreview,
activeBlockId: options.activeBlockId
activeBlockId: options.activeBlockId,
tabId
},
!options.silent
options.silent
);
}
}
}
};
openNextSession = () => {
const { sessions, activeSessionId } = this.get();
if (sessions.length === 0 || sessions.length === 1) return;
focusNextTab = () => {
const { tabs, activeTabId } = this.get();
if (tabs.length <= 1) return;
const index = sessions.findIndex((s) => s.id === activeSessionId);
const index = tabs.findIndex((s) => s.id === activeTabId);
if (index === -1) return;
if (index === sessions.length - 1) {
return this.openSession(sessions[0].id);
}
return this.openSession(sessions[index + 1].id);
return this.focusTab(tabs[index === tabs.length - 1 ? 0 : index + 1].id);
};
openPreviousSession = () => {
const { sessions, activeSessionId } = this.get();
if (sessions.length === 0 || sessions.length === 1) return;
focusPreviousTab = () => {
const { tabs, activeTabId } = this.get();
if (tabs.length <= 1) return;
const index = sessions.findIndex((s) => s.id === activeSessionId);
const index = tabs.findIndex((s) => s.id === activeTabId);
if (index === -1) return;
if (index === 0) {
return this.openSession(sessions[sessions.length - 1].id);
}
return this.openSession(sessions[index - 1].id);
return this.focusTab(tabs[index === 0 ? tabs.length - 1 : index - 1].id);
};
addSession = (session: EditorSession, activate = true) => {
let oldSessionId: string | null = null;
goBack = async () => {
const activeTabId = this.get().activeTabId;
if (!activeTabId || !tabSessionHistory.canGoBack(activeTabId)) return;
const sessionId = tabSessionHistory.back(activeTabId);
if (!sessionId) return;
if (!(await this.goToSession(activeTabId, sessionId))) {
await this.goBack();
}
};
goForward = async () => {
const activeTabId = this.get().activeTabId;
if (!activeTabId || !tabSessionHistory.canGoForward(activeTabId)) return;
const sessionId = tabSessionHistory.forward(activeTabId);
if (!sessionId) return;
if (!(await this.goToSession(activeTabId, sessionId))) {
await this.goForward();
}
};
goToSession = async (tabId: string, sessionId: string) => {
const session = this.get().getSession(sessionId);
if (!session) {
tabSessionHistory.remove(tabId, sessionId);
return false;
}
if (session.type === "new") {
this.activateSession(session.id);
return true;
} else {
if (!(await db.notes.exists(session.note.id))) {
tabSessionHistory.remove(tabId, session.id);
this.set((state) => {
const index = state.sessions.findIndex((s) => s.id === session.id);
state.sessions.splice(index, 1);
});
return false;
}
// we must rehydrate the session as the note's content can be stale
this.updateSession(session.id, undefined, {
needsHydration: true
});
this.activateSession(session.id);
return true;
}
};
addSession = (session: EditorSession, silent = false) => {
this.set((state) => {
const { activeSessionIndex, duplicateSessionIndex, previewSessionIndex } =
findSessionIndices(state.sessions, session, state.activeSessionId);
if (duplicateSessionIndex > -1) {
oldSessionId = state.sessions[duplicateSessionIndex].id;
state.sessions[duplicateSessionIndex] = session;
} else if (previewSessionIndex > -1) {
oldSessionId = state.sessions[previewSessionIndex].id;
state.sessions[previewSessionIndex] = session;
} else if (activeSessionIndex > -1)
state.sessions.splice(activeSessionIndex + 1, 0, session);
else state.sessions.push(session);
state.sessions.sort((a, b) =>
a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1
);
const index = state.sessions.findIndex((s) => s.id === session.id);
if (index > -1) {
state.sessions[index] = session;
} else state.sessions.push(session);
});
const { history } = this.get();
if (
oldSessionId &&
oldSessionId !== session.id &&
history.includes(oldSessionId)
)
history.splice(history.indexOf(oldSessionId), 1);
if (activate) this.activateSession(session.id);
this.activateSession(session.id, undefined, silent);
};
saveSession = async (
@@ -786,6 +899,10 @@ class EditorStore extends BaseStore<EditorStore> {
this.setSaveState(id, 0);
try {
const sessionId = getSessionId(currentSession);
const noteId =
("note" in currentSession
? currentSession.note.id
: partial.note?.id) || id;
if (isLockedSession(currentSession) && partial.content) {
logger.debug("Saving locked content", { id });
@@ -793,7 +910,7 @@ class EditorStore extends BaseStore<EditorStore> {
await db.vault.save({
content: partial.content,
sessionId,
id
id: noteId
});
} else {
if (partial.content)
@@ -813,36 +930,36 @@ class EditorStore extends BaseStore<EditorStore> {
: undefined,
content: partial.content,
sessionId,
id
id: noteId
});
}
const note = await db.notes.note(id);
const note = await db.notes.note(noteId);
if (!note) throw new Error("Note not saved.");
if (currentSession.type === "new") {
if (currentSession.context) {
const { type } = currentSession.context;
const context = useNoteStore.getState().context;
if (context) {
const { type } = context;
if (type === "notebook")
await db.notes.addToNotebook(currentSession.context.id, id);
await db.notes.addToNotebook(context.id, noteId);
else if (type === "color" || type === "tag")
await db.relations.add(
{ type, id: currentSession.context.id },
{ type, id: context.id },
{ id, type: "note" }
);
} else {
const defaultNotebook = db.settings.getDefaultNotebook();
if (defaultNotebook)
await db.notes.addToNotebook(defaultNotebook, id);
await db.notes.addToNotebook(defaultNotebook, noteId);
}
}
const attachmentsLength = await db.attachments
.ofNote(id, "all")
.ofNote(note.id, "all")
.count();
const shouldRefreshNotes =
currentSession.type === "new" ||
!id ||
note.title !== currentSession.note?.title ||
note.headline !== currentSession.note?.headline ||
attachmentsLength !== currentSession.attachmentsLength;
@@ -869,7 +986,6 @@ class EditorStore extends BaseStore<EditorStore> {
}
this.updateSession(id, ["default"], {
preview: false,
attachmentsLength: attachmentsLength,
note,
sessionId
@@ -889,8 +1005,8 @@ class EditorStore extends BaseStore<EditorStore> {
this.setSaveState(id, SaveState.NotSaved);
console.error(err);
if (err instanceof Error) logger.error(err);
if (isLockedSession(currentSession)) {
this.get().openSession(id, { force: true });
if (isLockedSession(currentSession) && "note" in currentSession) {
this.get().openSession(currentSession.note, { force: true });
}
}
});
@@ -916,43 +1032,66 @@ class EditorStore extends BaseStore<EditorStore> {
};
newSession = () => {
const state = useEditorStore.getState();
const session = state.sessions.find((session) => session.type === "new");
if (session) {
session.context = useNoteStore.getState().context;
this.activateSession(session.id);
} else {
this.addSession({
type: "new",
id: getId(),
context: useNoteStore.getState().context,
saveState: SaveState.NotSaved
});
const { activeTabId, activateSession } = this.get();
if (!activeTabId) {
this.addTab();
return;
}
const session = this.getActiveSession();
if (session?.type === "new") return activateSession(session.id);
const sessionId = tabSessionHistory.add(activeTabId);
this.addSession({
type: "new",
id: sessionId,
tabId: activeTabId,
saveState: SaveState.NotSaved
});
};
closeSessions = (...ids: string[]) => {
closeActiveTab = () => {
const { activeTabId } = this.get();
if (!activeTabId) return;
this.closeTabs(activeTabId);
};
closeAllTabs = () => {
const { tabs } = this.get();
this.closeTabs(...tabs.map((t) => t.id));
};
closeTabs = (...ids: string[]) => {
this.set((state) => {
const sessions: EditorSession[] = [];
for (let i = 0; i < state.sessions.length; ++i) {
const session = state.sessions[i];
if (!ids.includes(session.id)) {
sessions.push(session);
const tabs: TabItem[] = [];
for (let i = 0; i < state.tabs.length; ++i) {
const tab = state.tabs[i];
if (!ids.includes(tab.id)) {
tabs.push(tab);
continue;
}
this.saveSessionContentIfNotSaved(session.id);
this.saveSessionContentIfNotSaved(tab.sessionId);
db.fs().cancel(session.id).catch(console.error);
if (state.history.includes(session.id))
state.history.splice(state.history.indexOf(session.id), 1);
db.fs().cancel(tab.sessionId).catch(console.error);
if (state.history.includes(tab.id))
state.history.splice(state.history.indexOf(tab.id), 1);
const tabHistory = tabSessionHistory.getTabHistory(tab.id);
state.sessions = state.sessions.filter((session) => {
return (
!tabHistory.back.includes(session.id) &&
!tabHistory.forward.includes(session.id)
);
});
tabSessionHistory.clearStackForTab(tab.id);
}
state.sessions = sessions;
state.tabs = tabs;
});
const { history, sessions } = this.get();
this.activateSession(history.pop());
if (sessions.length === 0) this.newSession();
const { history, tabs } = this.get();
this.focusTab(history.pop());
if (tabs.length === 0) this.addTab();
};
setTitle = (id: string, title: string) => {
@@ -1004,23 +1143,90 @@ class EditorStore extends BaseStore<EditorStore> {
this.set({ editorMargins: editorMarginsState });
Config.set("editor:margins", editorMarginsState);
};
getActiveTab = () => {
const activeTabId = this.get().activeTabId;
return this.get().tabs.find((t) => t.id === activeTabId);
};
getActiveNote = () => {
const session = this.getActiveSession();
return session && "note" in session ? session.note : undefined;
};
isNoteOpen = (noteId: string) => {
return this.getActiveNote()?.id === noteId;
};
getActiveSession = <T extends SessionType[]>(
types?: T
): SessionTypeMap[T[number]] | undefined => {
const activeTab = this.getActiveTab();
if (!activeTab) return;
const session = this.getSession(activeTab.sessionId);
if (session && (!types || types.includes(session.type)))
return session as SessionTypeMap[T[number]];
};
addTab = (sessionId?: string) => {
const id = getId();
const newSessionId = sessionId || tabSessionHistory.add(id);
this.set((state) => {
state.tabs.push({
id,
sessionId: newSessionId
});
});
if (!sessionId)
this.addSession({
type: "new",
tabId: id,
id: newSessionId,
saveState: SaveState.NotSaved
});
this.focusTab(id);
return id;
};
focusTab = (tabId: string | undefined, sessionId?: string) => {
if (!tabId) return;
const { history } = this.get();
if (history.includes(tabId)) history.splice(history.indexOf(tabId), 1);
history.push(tabId);
this.set({
activeTabId: tabId,
canGoBack: tabSessionHistory.canGoBack(tabId),
canGoForward: tabSessionHistory.canGoForward(tabId)
});
sessionId =
sessionId || this.get().tabs.find((t) => t.id === tabId)?.sessionId;
if (sessionId) this.rehydrateSession(sessionId);
};
}
const useEditorStore = createPersistedStore(EditorStore, {
name: "editor-sessions",
name: "editor-sessions-v2",
partialize: (state) => ({
history: state.history,
activeSessionId: state.activeSessionId,
arePropertiesVisible: state.arePropertiesVisible,
editorMargins: state.editorMargins,
tabs: state.tabs,
activeTabId: state.activeTabId,
tabHistory: state.tabHistory,
canGoBack: state.canGoBack,
canGoForward: state.canGoForward,
sessions: state.sessions.reduce((sessions, session) => {
sessions.push({
id: session.id,
type: isLockedSession(session) ? "locked" : session.type,
needsHydration: session.type === "new" ? false : true,
preview: session.preview,
pinned: session.pinned,
title: session.title,
historySessionId:
session.type === "diff" ? session.historySessionId : undefined,
tabId: session.tabId,
note:
"note" in session
? {
@@ -1037,28 +1243,6 @@ const useEditorStore = createPersistedStore(EditorStore, {
});
export { useEditorStore, SESSION_STATES };
function findSessionIndices(
sessions: EditorSession[],
session: EditorSession,
activeSessionId?: string
) {
let activeSessionIndex = -1;
let previewSessionIndex = -1;
let duplicateSessionIndex = -1;
for (let i = 0; i < sessions.length; ++i) {
const { id, preview } = sessions[i];
if (id === session.id) duplicateSessionIndex = i;
else if (preview && session.preview) previewSessionIndex = i;
else if (id === activeSessionId) activeSessionIndex = i;
}
return {
activeSessionIndex,
previewSessionIndex,
duplicateSessionIndex
};
}
const MILLISECONDS_IN_A_MINUTE = 60 * 1000;
const SESSION_DURATION = MILLISECONDS_IN_A_MINUTE * 5;
function getSessionId(session: DefaultEditorSession | NewEditorSession) {

View File

@@ -68,7 +68,7 @@ async function resetReminders(reminders: FilteredSelector<Reminder>) {
)
return;
for await (const reminder of reminders) {
for await (const reminder of reminders.iterate()) {
if (reminder.disabled) continue;
// set a one time reminder at the snoozed date

View File

@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import createStore from "../common/store";
import { db } from "../common/db";
import BaseStore from "./index";
import config from "../utils/config";
import { EV, EVENTS } from "@notesnook/core";
import Config from "../utils/config";
import { hashNavigate } from "../navigation";
@@ -81,7 +80,7 @@ class UserStore extends BaseStore<UserStore> {
state.user = undefined;
state.isLoggedIn = false;
});
config.clear();
Config.logout();
if (reason) {
await ConfirmDialog.show({
title: strings.loggedOut(),

View File

@@ -57,5 +57,30 @@ function has(predicate: (key: string) => boolean) {
return false;
}
const Config = { set, get, clear, all, has, remove };
function logout() {
const toKeep = [
"editorConfig",
"backupStorageLocation",
"serverUrls",
"corsProxy",
"theme:light",
"theme:dark",
"colorScheme",
"followSystemTheme"
];
const vals = {} as Record<string, any>;
for (const keep of toKeep) {
const val = get(keep);
if (val !== undefined) vals[keep] = val;
}
clear();
for (const key in vals) {
set(key, vals[key]);
}
}
const Config = { set, get, clear, all, has, remove, logout };
export default Config;

View File

@@ -63,7 +63,7 @@ export async function postMessage(
// so all download links needs to be prefixed to avoid any other conflict
data.origin = window.location.origin;
// if we ever (in some feature versoin of streamsaver) would like to
// if we ever (in some feature version of streamsaver) would like to
// redirect back to the page of who initiated a http request
data.referrer = data.referrer || document.referrer || origin;

View File

@@ -148,7 +148,10 @@ function SubNotebooks({ rootId, isCollapsed, onClick }: SubNotebooksProps) {
const toggleSelection = useSelectionStore(
(store) => store.toggleSelectionMode
);
const rootNotebook = usePromise(() => db.notebooks.notebook(rootId));
const rootNotebook = usePromise(
() => db.notebooks.notebook(rootId),
[rootId]
);
const notebooks = useNotebookStore((store) => store.notebooks);
useEffect(() => {

View File

@@ -0,0 +1,5 @@
- New and improved editor tabs with history
- Fixed homescreen widget bugs
- Many small bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,6 @@
- New and improved editor tabs with history
- Fixed a crash on app launch faced by some users
- Fixed homescreen note widget does not open correct note
- Many small bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,6 @@
- New and improved editor tabs with history
- Fixed a crash on app launch faced by some users
- Fixed homescreen note widget does not open correct note
- Many small bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -71,5 +71,6 @@
"license": "GPL-3.0-or-later",
"volta": {
"node": "20.12.0"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

Some files were not shown because too many files have changed in this diff Show More