mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 11:39:21 +02:00
Compare commits
19 Commits
fix/file-s
...
release/3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fe410ae89 | ||
|
|
4cbdd085d6 | ||
|
|
24556dab83 | ||
|
|
dd8116eba8 | ||
|
|
ae82a8c0a0 | ||
|
|
af4dc5e9ea | ||
|
|
71b03a90dd | ||
|
|
728e50cd13 | ||
|
|
c8e8935b34 | ||
|
|
21f4b906d7 | ||
|
|
d49d42b736 | ||
|
|
267fe4e453 | ||
|
|
413ca61ef5 | ||
|
|
02b96470bc | ||
|
|
7234128f78 | ||
|
|
27e070e555 | ||
|
|
539950bdc5 | ||
|
|
83280e7cee | ||
|
|
cee0275565 |
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.12",
|
||||
"version": "3.3.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.12",
|
||||
"version": "3.3.13",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.12",
|
||||
"version": "3.3.13",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
|
||||
@@ -199,11 +199,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".OnClearFromRecentService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
|
||||
|
||||
<service
|
||||
|
||||
@@ -28,10 +28,6 @@ public class MainActivity extends ReactActivity {
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
|
||||
try {
|
||||
startService(new Intent(getBaseContext(), OnClearFromRecentService.class));
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
public class OnClearFromRecentService extends Service {
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskRemoved(Intent rootIntent) {
|
||||
try {
|
||||
SharedPreferences appStateDetails = getApplicationContext().getSharedPreferences("appStateDetails", MODE_PRIVATE);
|
||||
SharedPreferences.Editor edit = appStateDetails.edit();
|
||||
edit.remove("appState");
|
||||
edit.apply();
|
||||
stopSelf();
|
||||
} catch (UnsatisfiedLinkError | Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,21 +93,6 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setAppState(final String appState) {
|
||||
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor edit = appStateDetails.edit();
|
||||
edit.putString("appState", appState);
|
||||
edit.apply();
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public String getAppState() {
|
||||
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
|
||||
String appStateValue = appStateDetails.getString("appState", "");
|
||||
return appStateValue.isEmpty() ? null : appStateValue;
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public int getWidgetId() {
|
||||
return NotePreviewConfigureActivity.appWidgetId;
|
||||
|
||||
@@ -40,7 +40,6 @@ import Animated, {
|
||||
withTiming
|
||||
} from "react-native-reanimated";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { getAppState } from "../../screens/editor/tiptap/utils";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
@@ -84,18 +83,9 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
}: TabProps,
|
||||
ref
|
||||
) {
|
||||
const appState = useMemo(() => getAppState(), []);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const fullscreen = useSettingStore((state) => state.fullscreen);
|
||||
const translateX = useSharedValue(
|
||||
widths
|
||||
? appState &&
|
||||
appState?.movedAway === false &&
|
||||
useTabStore.getState().getCurrentNoteId()
|
||||
? widths.sidebar + widths.list
|
||||
: widths.sidebar
|
||||
: 0
|
||||
);
|
||||
const translateX = useSharedValue(widths ? widths.sidebar : 0);
|
||||
const startX = useSharedValue(0);
|
||||
const currentTab = useSharedValue(1);
|
||||
const previousTab = useSharedValue(1);
|
||||
@@ -125,10 +115,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
translateX.value = 0;
|
||||
} else {
|
||||
if (prevWidths.current?.sidebar !== widths.sidebar) {
|
||||
translateX.value =
|
||||
appState && appState?.movedAway === false
|
||||
? editorPosition
|
||||
: widths.sidebar;
|
||||
translateX.value = widths.sidebar;
|
||||
if (translateX.value === editorPosition) {
|
||||
onChangeTab?.({ i: 2, from: 1 });
|
||||
}
|
||||
@@ -136,15 +123,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
}
|
||||
isLoaded.current = true;
|
||||
prevWidths.current = widths;
|
||||
}, [
|
||||
deviceMode,
|
||||
widths,
|
||||
fullscreen,
|
||||
translateX,
|
||||
editorPosition,
|
||||
appState,
|
||||
onChangeTab
|
||||
]);
|
||||
}, [deviceMode, widths, fullscreen, translateX, editorPosition, onChangeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
|
||||
|
||||
@@ -53,15 +53,9 @@ async function fetchMonographData(noteId: string) {
|
||||
const monograph = monographId
|
||||
? await db.monographs.get(monographId)
|
||||
: undefined;
|
||||
const analyticsFeature = await isFeatureAvailable("monographAnalytics");
|
||||
const analytics =
|
||||
monographId && analyticsFeature
|
||||
? await db.monographs.analytics(monographId)
|
||||
: undefined;
|
||||
return {
|
||||
monograph,
|
||||
monographId,
|
||||
analytics
|
||||
};
|
||||
}
|
||||
|
||||
@@ -381,9 +375,6 @@ const PublishNoteSheet = ({
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
|
||||
<Paragraph>
|
||||
{monographData?.result?.analytics?.totalViews || 0}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { strings } from "@notesnook/intl";
|
||||
import notifee from "@notifee/react-native";
|
||||
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
|
||||
import dayjs from "dayjs";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
AppState,
|
||||
@@ -48,6 +49,7 @@ import * as RNIap from "react-native-iap";
|
||||
import { DatabaseLogger, db, setupDatabase } from "../common/database";
|
||||
import { initializeLogger } from "../common/database/logger";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
import { deleteDCacheFiles } from "../common/filesystem/io";
|
||||
import { endProgress, startProgress } from "../components/dialogs/progress";
|
||||
import Migrate from "../components/sheets/migrate";
|
||||
import NewFeature from "../components/sheets/new-feature";
|
||||
@@ -58,11 +60,7 @@ import {
|
||||
resetTabStore,
|
||||
useTabStore
|
||||
} from "../screens/editor/tiptap/use-tab-store";
|
||||
import {
|
||||
clearAppState,
|
||||
editorController,
|
||||
editorState
|
||||
} from "../screens/editor/tiptap/utils";
|
||||
import { editorController, editorState } from "../screens/editor/tiptap/utils";
|
||||
import { useDragState } from "../screens/settings/editor/state";
|
||||
import BackupService from "../services/backup";
|
||||
import BiometricService from "../services/biometrics";
|
||||
@@ -89,6 +87,7 @@ import { clearAllStores, initAfterSync } from "../stores";
|
||||
import { refreshAllStores } from "../stores/create-db-collection-store";
|
||||
import { useAttachmentStore } from "../stores/use-attachment-store";
|
||||
import { useMessageStore } from "../stores/use-message-store";
|
||||
import { useRelationStore } from "../stores/use-relation-store";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { SyncStatus, useUserStore } from "../stores/use-user-store";
|
||||
import { updateStatusBarColor } from "../utils/colors";
|
||||
@@ -105,12 +104,8 @@ import {
|
||||
} from "../utils/events";
|
||||
import { getGithubVersion } from "../utils/github-version";
|
||||
import { fluidTabsRef } from "../utils/global-refs";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { sleep } from "../utils/time";
|
||||
import useFeatureManager from "./use-feature-manager";
|
||||
import { deleteDCacheFiles } from "../common/filesystem/io";
|
||||
import dayjs from "dayjs";
|
||||
import { useRelationStore } from "../stores/use-relation-store";
|
||||
|
||||
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
|
||||
const { disableSync, disableAutoSync } = SettingsService.get();
|
||||
@@ -173,7 +168,6 @@ const onAppOpenedFromURL = async (event: {
|
||||
if (url.startsWith("https://app.notesnook.com/account/verified")) {
|
||||
await onUserEmailVerified();
|
||||
} else if (url.startsWith("ShareMedia://QuickNoteWidget")) {
|
||||
clearAppState();
|
||||
editorState().movedAway = false;
|
||||
eSendEvent(eOnLoadNote, { newNote: true });
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
@@ -361,23 +355,6 @@ async function checkForShareExtensionLaunchedInBackground() {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function saveEditorState() {
|
||||
if (!editorState().movedAway) {
|
||||
const id = useTabStore.getState().getCurrentNoteId();
|
||||
const note = id ? await db.notes.note(id) : undefined;
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if (locked) return;
|
||||
const state = JSON.stringify({
|
||||
editing: editorState().currentlyEditing,
|
||||
movedAway: editorState().movedAway,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
NotesnookModule.setAppState(state);
|
||||
} else {
|
||||
NotesnookModule.setAppState("");
|
||||
}
|
||||
}
|
||||
|
||||
const onSuccessfulSubscription = async (
|
||||
subscription: RNIap.ProductPurchase | RNIap.SubscriptionPurchase
|
||||
) => {
|
||||
@@ -592,13 +569,13 @@ export const useAppEvents = () => {
|
||||
}, [isAppLoading, onSyncComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialUrl) {
|
||||
if (initialUrl && !isAppLoading) {
|
||||
onAppOpenedFromURL({
|
||||
url: initialUrl!,
|
||||
isInitialUrl: true
|
||||
});
|
||||
}
|
||||
}, [initialUrl]);
|
||||
}, [initialUrl, isAppLoading]);
|
||||
|
||||
const subscribeToPurchaseListeners = useCallback(async () => {
|
||||
if (Platform.OS === "android") {
|
||||
@@ -838,7 +815,6 @@ export const useAppEvents = () => {
|
||||
Sync.run("global", false, "full");
|
||||
reconnectSSE();
|
||||
await checkForShareExtensionLaunchedInBackground();
|
||||
NotesnookModule.setAppState("");
|
||||
let user = await db.user.getUser();
|
||||
if (user && !user?.isEmailConfirmed) {
|
||||
try {
|
||||
@@ -861,7 +837,6 @@ export const useAppEvents = () => {
|
||||
eSendEvent(eEditorReset);
|
||||
}
|
||||
} else {
|
||||
await saveEditorState();
|
||||
if (
|
||||
SettingsService.canLockAppInBackground() &&
|
||||
!useSettingStore.getState().requestBiometrics &&
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useEffect } from "react";
|
||||
import { NativeEventEmitter, NativeModule } from "react-native";
|
||||
import { useRef } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { Linking } from "react-native";
|
||||
import deviceInfoModule from "react-native-device-info";
|
||||
import { strings } from "@notesnook/intl";
|
||||
const ShortcutsEmitter = new NativeEventEmitter(
|
||||
@@ -54,14 +53,9 @@ export const useShortcutManager = ({
|
||||
}, [shortcuts]);
|
||||
|
||||
useEffect(() => {
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url?.startsWith("ShareMedia://QuickNoteWidget")) {
|
||||
onShortcutPressed(defaultShortcuts[0]);
|
||||
}
|
||||
});
|
||||
if (!isSupported()) return;
|
||||
Shortcuts.getInitialShortcut().then((shortcut) => {
|
||||
if (initialShortcutRecieved.current) return;
|
||||
if (initialShortcutRecieved.current || !shortcut) return;
|
||||
onShortcutPressed(shortcut);
|
||||
initialShortcutRecieved.current = true;
|
||||
});
|
||||
|
||||
@@ -47,12 +47,7 @@ import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
|
||||
import { useShortcutManager } from "../hooks/use-shortcut-manager";
|
||||
import { hideAllTooltips } from "../hooks/use-tooltip";
|
||||
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
|
||||
import {
|
||||
clearAppState,
|
||||
editorController,
|
||||
editorState,
|
||||
getAppState
|
||||
} from "../screens/editor/tiptap/utils";
|
||||
import { editorController, editorState } from "../screens/editor/tiptap/utils";
|
||||
import { DDS } from "../services/device-detection";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -118,13 +113,9 @@ export const FluidPanelsView = React.memo(
|
||||
|
||||
useShortcutManager({
|
||||
onShortcutPressed: async (item) => {
|
||||
if (!item && getAppState()) {
|
||||
editorState().movedAway = false;
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
return;
|
||||
}
|
||||
if (!item) return;
|
||||
|
||||
if (item?.type === "notesnook.action.newnote") {
|
||||
clearAppState();
|
||||
if (!fluidTabsRef.current) {
|
||||
setTimeout(() => {
|
||||
eSendEvent(eOnLoadNote, { newNote: true });
|
||||
@@ -205,7 +196,6 @@ export const FluidPanelsView = React.memo(
|
||||
eSendEvent(eCloseFullscreenEditor, current);
|
||||
}
|
||||
|
||||
const state = getAppState();
|
||||
setTimeout(() => {
|
||||
switch (current) {
|
||||
case "tablet":
|
||||
@@ -217,18 +207,10 @@ export const FluidPanelsView = React.memo(
|
||||
}
|
||||
break;
|
||||
case "mobile":
|
||||
if (
|
||||
state &&
|
||||
editorState().movedAway === false &&
|
||||
useTabStore.getState().getCurrentNoteId()
|
||||
) {
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
} else {
|
||||
fluidTabsRef.current?.goToPage(
|
||||
fluidTabsRef.current?.page(),
|
||||
false
|
||||
);
|
||||
}
|
||||
fluidTabsRef.current?.goToPage(
|
||||
fluidTabsRef.current?.page(),
|
||||
false
|
||||
);
|
||||
break;
|
||||
}
|
||||
}, 0);
|
||||
@@ -243,8 +225,8 @@ export const FluidPanelsView = React.memo(
|
||||
const nextDeviceMode = DDS.isLargeTablet()
|
||||
? "tablet"
|
||||
: DDS.isSmallTab
|
||||
? "smallTablet"
|
||||
: "mobile";
|
||||
? "smallTablet"
|
||||
: "mobile";
|
||||
setDeviceMode(nextDeviceMode, size);
|
||||
},
|
||||
[orientation, setDeviceMode, setDimensions]
|
||||
|
||||
@@ -471,9 +471,8 @@ export const useEditorEvents = (
|
||||
relationType: "from",
|
||||
title: strings.dataTypesPluralCamelCase.reminder(),
|
||||
onAdd: async () => {
|
||||
const reminderFeature = await isFeatureAvailable(
|
||||
"activeReminders"
|
||||
);
|
||||
const reminderFeature =
|
||||
await isFeatureAvailable("activeReminders");
|
||||
if (!reminderFeature.isAllowed) {
|
||||
ToastManager.show({
|
||||
type: "info",
|
||||
@@ -560,7 +559,7 @@ export const useEditorEvents = (
|
||||
.then((data: any) => {
|
||||
console.log(
|
||||
"Got attachment data:",
|
||||
data,
|
||||
!!data,
|
||||
editorMessage.resolverId
|
||||
);
|
||||
editor.postMessage(NativeEvents.resolve, {
|
||||
|
||||
@@ -69,14 +69,7 @@ import Commands from "./commands";
|
||||
import { SessionHistory } from "./session-history";
|
||||
import { EditorState, SavePayload } from "./types";
|
||||
import { TabSessionItem, syncTabs, useTabStore } from "./use-tab-store";
|
||||
import {
|
||||
clearAppState,
|
||||
defaultState,
|
||||
getAppState,
|
||||
isContentInvalid,
|
||||
isEditorLoaded,
|
||||
post
|
||||
} from "./utils";
|
||||
import { defaultState, isContentInvalid, isEditorLoaded, post } from "./utils";
|
||||
import { Linking } from "react-native";
|
||||
|
||||
const loadNoteMutex = new Mutex();
|
||||
@@ -167,7 +160,7 @@ export const useEditor = (
|
||||
const tags = useTagStore((state) => state.items);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const isDefaultEditor = editorId === "";
|
||||
const saveCount = useRef(0);
|
||||
const saveCount = useRef<Record<string, number>>({});
|
||||
const lastContentChangeTime = useRef<Record<string, number>>({});
|
||||
const lock = useRef(false);
|
||||
const currentLoadingNoteId = useRef<string>(undefined);
|
||||
@@ -253,9 +246,9 @@ export const useEditor = (
|
||||
clearTimeout(timers.current["loading-images" + noteId]);
|
||||
}
|
||||
|
||||
saveCount.current = 0;
|
||||
currentLoadingNoteId.current = undefined;
|
||||
lock.current = false;
|
||||
saveCount.current[tabId] = 0;
|
||||
resetContent && postMessage(NativeEvents.title, "", tabId);
|
||||
resetContent && (await commands.clearContent(tabId));
|
||||
resetContent && (await commands.clearTags(tabId));
|
||||
@@ -436,10 +429,9 @@ export const useEditor = (
|
||||
lastContentChangeTime.current[id] = note.dateEdited;
|
||||
|
||||
if (
|
||||
saveCount.current < 2 ||
|
||||
saveCount.current[tabId] < 2 ||
|
||||
currentNotes.current[id]?.title !== note.title ||
|
||||
currentNotes.current[id]?.headline?.slice(0, 200) !==
|
||||
note.headline?.slice(0, 200)
|
||||
currentNotes.current[id]?.headline !== note.headline
|
||||
) {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}
|
||||
@@ -455,7 +447,11 @@ export const useEditor = (
|
||||
currentNotes.current[id] = note;
|
||||
}
|
||||
|
||||
saveCount.current++;
|
||||
if (!saveCount.current[tabId]) {
|
||||
saveCount.current[tabId] = 1;
|
||||
} else {
|
||||
saveCount.current[tabId]++;
|
||||
}
|
||||
|
||||
clearTimeout(timers.current.onsave);
|
||||
timers.current.onsave = setTimeout(async () => {
|
||||
@@ -564,6 +560,7 @@ export const useEditor = (
|
||||
useTabStore.getState().newTabSession(tabId, {});
|
||||
}
|
||||
}
|
||||
saveCount.current[tabId] = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
if (state.current?.ready && !state.current.movedAway)
|
||||
@@ -696,6 +693,7 @@ export const useEditor = (
|
||||
currentNotes.current[item.id] = item;
|
||||
|
||||
editorSessionHistory.newSession(item.id);
|
||||
saveCount.current[tabId] = 0;
|
||||
|
||||
await commands.setStatus(
|
||||
getFormattedDate(item.dateEdited, "date-time"),
|
||||
@@ -1025,22 +1023,11 @@ export const useEditor = (
|
||||
);
|
||||
|
||||
const restoreEditorState = useCallback(async () => {
|
||||
const appState = getAppState();
|
||||
if (!appState) return;
|
||||
state.current.isRestoringState = true;
|
||||
state.current.currentlyEditing = true;
|
||||
if (fluidTabsRef.current?.page() === "editor") {
|
||||
state.current.movedAway = false;
|
||||
}
|
||||
if (!state.current.editorStateRestored) {
|
||||
state.current.isRestoringState = true;
|
||||
if (!DDS.isTab) {
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
}
|
||||
}
|
||||
|
||||
clearAppState();
|
||||
state.current.isRestoringState = false;
|
||||
}, []);
|
||||
|
||||
const onContentChanged = (noteId?: string) => {
|
||||
|
||||
@@ -141,34 +141,6 @@ export function isContentInvalid(content: string | undefined) {
|
||||
);
|
||||
}
|
||||
|
||||
const canRestoreAppState = (appState: AppState) => {
|
||||
return appState.editing && Date.now() < appState.timestamp + 3600000;
|
||||
};
|
||||
|
||||
let appState: AppState | undefined;
|
||||
export function setAppState(state: AppState) {
|
||||
appState = state;
|
||||
}
|
||||
export function getAppState() {
|
||||
if (appState && canRestoreAppState(appState)) return appState as AppState;
|
||||
const json = NotesnookModule.getAppState();
|
||||
if (json) {
|
||||
appState = JSON.parse(json) as AppState;
|
||||
if (canRestoreAppState(appState)) {
|
||||
return appState;
|
||||
} else {
|
||||
clearAppState();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function clearAppState() {
|
||||
appState = undefined;
|
||||
NotesnookModule.setAppState("");
|
||||
}
|
||||
|
||||
export async function openInternalLink(url: string) {
|
||||
const data = parseInternalLink(url);
|
||||
if (!data?.id) return false;
|
||||
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Platform } from "react-native";
|
||||
import { db } from "../common/database";
|
||||
import { setAppState } from "../screens/editor/tiptap/utils";
|
||||
import { eOnLoadNote } from "../utils/events";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { eSendEvent } from "./event-manager";
|
||||
@@ -37,14 +36,6 @@ export const IntentService = {
|
||||
onLaunch() {
|
||||
if (launched || Platform.OS === "ios") return;
|
||||
launched = true;
|
||||
if (launchIntent["com.streetwriters.notesnook.OpenNoteId"]) {
|
||||
setAppState({
|
||||
movedAway: false,
|
||||
editing: true,
|
||||
timestamp: Date.now(),
|
||||
noteId: launchIntent["com.streetwriters.notesnook.OpenNoteId"]
|
||||
});
|
||||
}
|
||||
},
|
||||
async onAppStateChanged() {
|
||||
if (Platform.OS === "ios") return;
|
||||
|
||||
@@ -138,7 +138,6 @@ const onEvent = async ({ type, detail }: Event) => {
|
||||
if (type === EventType.PRESS) {
|
||||
notifee.decrementBadgeCount();
|
||||
if (notification?.data?.type === "quickNote") return;
|
||||
NotesnookModule.setAppState("");
|
||||
if (notification?.data?.type === "reminder" && notification?.id) {
|
||||
const reminder = await db.reminders?.reminder(
|
||||
notification.id?.split("_")[0]
|
||||
@@ -453,13 +452,6 @@ async function loadNote(id: string, jump: boolean) {
|
||||
if (!DDS.isTab && jump) {
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
}
|
||||
NotesnookModule.setAppState(
|
||||
JSON.stringify({
|
||||
editing: true,
|
||||
movedAway: false,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
);
|
||||
|
||||
const tab = useTabStore.getState().getTabForNote(id);
|
||||
if (useTabStore.getState().currentTab !== tab) {
|
||||
|
||||
@@ -30,8 +30,6 @@ interface NotesnookModuleInterface {
|
||||
getActivityName: () => Promise<string>;
|
||||
setBackgroundColor: (color: string) => void;
|
||||
setSecureMode: (enabled: boolean) => void;
|
||||
setAppState: (appState: string) => void;
|
||||
getAppState: () => string;
|
||||
saveAndFinish: () => void;
|
||||
setString: (storeName: string, key: string, value: string) => void;
|
||||
getString: (storeName: string, key: string) => Promise<string>;
|
||||
@@ -72,8 +70,6 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({
|
||||
getActivityName: () => {},
|
||||
setBackgroundColor: () => {},
|
||||
setSecureMode: () => {},
|
||||
setAppState: () => {},
|
||||
getAppState: () => {},
|
||||
saveAndFinish: () => {},
|
||||
getString: () => {},
|
||||
setString: () => {},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"postinstall": "patch-package && node ./scripts/optimize-fonts.mjs",
|
||||
"run-android": "react-native run-android --active-arch-only",
|
||||
"run-android-release": "LOCAL_RELEASE_BUILD=true react-native run-android --active-arch-only --mode=release",
|
||||
"run-android-release": "STAGING_BUILD=true react-native run-android --active-arch-only --mode=release",
|
||||
"build": "echo 'No build script defined'",
|
||||
"run-ios": "react-native run-ios",
|
||||
"start": "react-native start",
|
||||
|
||||
File diff suppressed because one or more lines are too long
4
apps/web/package-lock.json
generated
4
apps/web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.3.12",
|
||||
"version": "3.3.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.3.12",
|
||||
"version": "3.3.13",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.12",
|
||||
"version": "3.3.13",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
|
||||
@@ -571,25 +571,39 @@ export function Editor(props: EditorProps) {
|
||||
onChange={onSave}
|
||||
onDownloadAttachment={(attachment) => saveAttachment(attachment.hash)}
|
||||
onPreviewAttachment={async (data) => {
|
||||
const { hash, type, mime } = data;
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
if (attachment && mime.startsWith("image/")) {
|
||||
await previewImageAttachment(attachment);
|
||||
} else if (
|
||||
attachment &&
|
||||
onPreviewDocument &&
|
||||
type === "file" &&
|
||||
attachment.mimeType.startsWith("application/pdf")
|
||||
) {
|
||||
onPreviewDocument({ hash });
|
||||
const blob = await downloadAttachment(hash, "blob", id);
|
||||
if (!blob) {
|
||||
useEditorStore.setState({ documentPreview: undefined });
|
||||
return;
|
||||
try {
|
||||
const { hash, type } = data;
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment)
|
||||
throw new Error("No attachment found with hash: " + hash);
|
||||
if (attachment.mimeType.startsWith("image/")) {
|
||||
await previewImageAttachment(attachment);
|
||||
} else if (
|
||||
onPreviewDocument &&
|
||||
type === "file" &&
|
||||
attachment.mimeType.startsWith("application/pdf")
|
||||
) {
|
||||
onPreviewDocument({ hash });
|
||||
const blob = await downloadAttachment(hash, "blob", id);
|
||||
if (!blob) {
|
||||
useEditorStore.setState({ documentPreview: undefined });
|
||||
return;
|
||||
}
|
||||
onPreviewDocument({ url: URL.createObjectURL(blob), hash });
|
||||
} else {
|
||||
logger.info("Attachment cannot be previewed", {
|
||||
hash,
|
||||
type,
|
||||
mimeType: attachment?.mimeType
|
||||
});
|
||||
showToast("error", strings.attachmentPreviewFailed());
|
||||
}
|
||||
onPreviewDocument({ url: URL.createObjectURL(blob), hash });
|
||||
} else {
|
||||
showToast("error", strings.attachmentPreviewFailed());
|
||||
} catch (e) {
|
||||
logger.error(e, "Failed to preview attachment", { data });
|
||||
showToast(
|
||||
"error",
|
||||
(e as Error).message || strings.attachmentPreviewFailed()
|
||||
);
|
||||
}
|
||||
}}
|
||||
onInsertAttachment={async (type) => {
|
||||
|
||||
@@ -17,7 +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 { HighlightedResult } from "@notesnook/core";
|
||||
import { HighlightedResult, Match as TMatch } from "@notesnook/core";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
@@ -112,11 +112,7 @@ function SearchResult(props: SearchResultProps) {
|
||||
}}
|
||||
>
|
||||
{item.title.map((match) => (
|
||||
<Fragment key={match.id}>
|
||||
<span>{match.prefix}</span>
|
||||
<span className="match">{match.match}</span>
|
||||
{match.suffix ? <span>{match.suffix}</span> : null}
|
||||
</Fragment>
|
||||
<Match match={match} />
|
||||
))}
|
||||
</Text>
|
||||
|
||||
@@ -171,11 +167,7 @@ function SearchResult(props: SearchResultProps) {
|
||||
}}
|
||||
>
|
||||
{match.map((match) => (
|
||||
<Fragment key={match.id}>
|
||||
<span>{match.prefix}</span>
|
||||
<span className="match">{match.match}</span>
|
||||
{match.suffix ? <span>{match.suffix}</span> : null}
|
||||
</Fragment>
|
||||
<Match match={match} />
|
||||
))}
|
||||
</Text>
|
||||
}
|
||||
@@ -193,6 +185,16 @@ function SearchResult(props: SearchResultProps) {
|
||||
|
||||
export default React.memo(SearchResult);
|
||||
|
||||
function Match({ match }: { match: TMatch }) {
|
||||
return (
|
||||
<Fragment key={match.id}>
|
||||
<span>{match.prefix}</span>
|
||||
<span className="match">{match.match}</span>
|
||||
{match.suffix ? <span>{match.suffix}</span> : null}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
async function menuItems(
|
||||
item: HighlightedResult,
|
||||
ids?: string[]
|
||||
|
||||
@@ -89,7 +89,22 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
)
|
||||
}
|
||||
]
|
||||
: [],
|
||||
: [
|
||||
{
|
||||
title: "Improved UX on password reset flow",
|
||||
subtitle:
|
||||
"We've added some minor UX improvements to the password reset flow"
|
||||
},
|
||||
{
|
||||
title: "Title $headline$ format change",
|
||||
subtitle:
|
||||
"The $headline$ format will now only be extracted from the first paragraph/heading of the note (up to 60 characters)."
|
||||
},
|
||||
{
|
||||
title: "Various bug fixes",
|
||||
subtitle: "We have squashed various bugs in this release."
|
||||
}
|
||||
],
|
||||
cta: {
|
||||
title: strings.gotIt(),
|
||||
icon: Checkmark,
|
||||
|
||||
@@ -286,14 +286,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const clearIds: string[] = [];
|
||||
for (const session of sessions) {
|
||||
if (session.type === "new") continue;
|
||||
const noteId = isDeleted(item)
|
||||
? null
|
||||
: item.type === "note"
|
||||
? item.id
|
||||
: item.type === "tiptap"
|
||||
? item.noteId
|
||||
: null;
|
||||
if (noteId && session.note.id !== noteId) continue;
|
||||
if (session.note.id !== item.id && session.note.contentId !== item.id) continue;
|
||||
if (isDeleted(item) || isTrashItem(item))
|
||||
clearIds.push(session.tabId);
|
||||
// if a note becomes conflicted, reopen the session
|
||||
|
||||
@@ -43,6 +43,7 @@ type NewPasswordFormData = {
|
||||
userResetRequired?: boolean;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
recoveryKey?: string;
|
||||
};
|
||||
|
||||
type RecoveryFormData = {
|
||||
@@ -304,7 +305,7 @@ function RecoveryMethods(props: BaseRecoveryComponentProps<"methods">) {
|
||||
}
|
||||
|
||||
function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
|
||||
const { navigate } = props;
|
||||
const { navigate, formData } = props;
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -329,14 +330,17 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
|
||||
subtitle: strings.keyRecoveryProgressDesc()
|
||||
}}
|
||||
onSubmit={async (form) => {
|
||||
const recoveryKey = form.recoveryKey;
|
||||
if (recoveryKey.length < 40) {
|
||||
throw new Error(strings.invalidRecoveryKey());
|
||||
}
|
||||
|
||||
setProgress(0);
|
||||
|
||||
const user = await db.user.getUser();
|
||||
if (!user) throw new Error(strings.notLoggedIn());
|
||||
await useKeyStore
|
||||
.getState()
|
||||
.setValue("userEncryptionKey", form.recoveryKey);
|
||||
navigate("new", {});
|
||||
await useKeyStore.getState().setValue("userEncryptionKey", recoveryKey);
|
||||
navigate("new", form);
|
||||
}}
|
||||
>
|
||||
<AuthField
|
||||
@@ -346,8 +350,19 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
|
||||
helpText={strings.enterRecoveryKeyHelp()}
|
||||
autoComplete="none"
|
||||
autoFocus
|
||||
defaultValue={formData?.recoveryKey || ""}
|
||||
/>
|
||||
<SubmitButton text={strings.startAccountRecovery()} />
|
||||
<Flex sx={{ gap: 1 }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
sx={{ mt: 50, borderRadius: 50 }}
|
||||
onClick={() => navigate("methods")}
|
||||
>
|
||||
{strings.back()}
|
||||
</Button>
|
||||
<SubmitButton text={strings.startAccountRecovery()} />
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
@@ -386,18 +401,29 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
|
||||
subtitle: strings.resetPasswordWait()
|
||||
}}
|
||||
onSubmit={async (form) => {
|
||||
setProgress(0);
|
||||
try {
|
||||
setProgress(0);
|
||||
|
||||
if (form.password !== form.confirmPassword)
|
||||
throw new Error("Passwords do not match.");
|
||||
if (form.password !== form.confirmPassword)
|
||||
throw new Error("Passwords do not match.");
|
||||
|
||||
if (formData?.userResetRequired && !(await db.user.resetUser()))
|
||||
throw new Error("Failed to reset user.");
|
||||
if (formData?.userResetRequired && !(await db.user.resetUser()))
|
||||
throw new Error("Failed to reset user.");
|
||||
|
||||
if (!(await db.user.resetPassword(form.password)))
|
||||
throw new Error("Could not reset account password.");
|
||||
if (!(await db.user.resetPassword(form.password)))
|
||||
throw new Error("Could not reset account password.");
|
||||
|
||||
navigate("final");
|
||||
navigate("final");
|
||||
} catch (e) {
|
||||
if ((e as Error).message === "invalid input") {
|
||||
console.error(e);
|
||||
throw new Error(
|
||||
"Password reset failed because of invalid recovery key"
|
||||
);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(form?: NewPasswordFormData) => (
|
||||
@@ -417,7 +443,22 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
|
||||
label={strings.confirmPassword()}
|
||||
defaultValue={form?.confirmPassword}
|
||||
/>
|
||||
<SubmitButton text={strings.continue()} />
|
||||
<Flex sx={{ gap: 1 }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
sx={{ mt: 50, borderRadius: 50 }}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
formData?.userResetRequired ? "methods" : "method:key",
|
||||
formData
|
||||
)
|
||||
}
|
||||
>
|
||||
{strings.back()}
|
||||
</Button>
|
||||
<SubmitButton text={strings.continue()} />
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</RecoveryForm>
|
||||
@@ -425,39 +466,20 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
|
||||
}
|
||||
|
||||
function Final(_props: BaseRecoveryComponentProps<"final">) {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
useEffect(() => {
|
||||
async function finalize() {
|
||||
await RecoveryKeyDialog.show({});
|
||||
if (!isSessionExpired()) {
|
||||
await db.user.logout(true, "Password changed.");
|
||||
if (isSessionExpired()) {
|
||||
openURL("/sessionexpired");
|
||||
} else {
|
||||
await db.user.clearSessions(true);
|
||||
openURL("/login");
|
||||
}
|
||||
setIsReady(true);
|
||||
}
|
||||
finalize();
|
||||
}, []);
|
||||
|
||||
if (!isReady && !isSessionExpired)
|
||||
return <Loader text="" title={strings.loading() + "..."} />;
|
||||
|
||||
return (
|
||||
<RecoveryForm
|
||||
testId="step-finished"
|
||||
type="final"
|
||||
title={strings.recoverySuccess()}
|
||||
subtitle={strings.recoverySuccessDesc()}
|
||||
onSubmit={async () => {
|
||||
openURL(isSessionExpired() ? "/sessionexpired" : "/login");
|
||||
}}
|
||||
>
|
||||
<SubmitButton
|
||||
text={
|
||||
isSessionExpired() ? strings.continue() : strings.loginToYourAccount()
|
||||
}
|
||||
/>
|
||||
</RecoveryForm>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
type RecoveryFormProps<TType extends RecoveryRoutes> = {
|
||||
@@ -537,7 +559,7 @@ export function RecoveryForm<T extends RecoveryRoutes>(
|
||||
{subtitle}
|
||||
</Text>
|
||||
{typeof children === "function" ? children(form) : children}
|
||||
<ErrorText error={error} />
|
||||
<ErrorText error={error} sx={{ mt: 2 }} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
docs/help/contents/_include/plus-reminder-icon.png
Normal file
BIN
docs/help/contents/_include/plus-reminder-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -28,7 +28,14 @@ Notesnook allows you to delete your accounts and your data without any questions
|
||||
|
||||
### What information is deleted?
|
||||
|
||||
All your notes, notebooks, attachments, and any other data that you created within the app will be immediately deleted.
|
||||
When you delete your account, **all** of the following will be permanently removed:
|
||||
|
||||
- All your notes, notebooks, attachments, and any other data you created within the app
|
||||
- Your account login credentials (email and password)
|
||||
- Your subscription and billing information
|
||||
- Your registered user account completely
|
||||
|
||||
After deletion, your email address will no longer be associated with Notesnook and cannot be used to log in.
|
||||
|
||||
### Is there any way to recover deleted data?
|
||||
|
||||
|
||||
100
docs/help/contents/reminders.md
Normal file
100
docs/help/contents/reminders.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Setting reminders on notes
|
||||
description: Learn how to create reminders on your notes in Notesnook. Set one-time or recurring reminders with custom notification preferences.
|
||||
---
|
||||
|
||||
# Creating reminders
|
||||
|
||||
Reminders help you stay on top of your important notes. You can create one-time or recurring reminders with custom notification settings.
|
||||
|
||||
## Adding a reminder to a note
|
||||
|
||||
# [Desktop/Web](#/tab/web)
|
||||
|
||||
1. Right click on a note to open the `Note properties` menu
|
||||
2. Click on `Remind me` to open the add reminder dialog
|
||||
3. Enter a title for the reminder and description (optional)
|
||||
4. Choose your reminder type:
|
||||
- **Once**: Set a specific date and time
|
||||
- **Repeat** (paid plans only): Create a recurring reminder (daily, weekly, monthly, or yearly)
|
||||
5. Select your notification preference (Silent, Vibrate, or Urgent)
|
||||
6. Click `Add` to save the reminder
|
||||
|
||||
# [Mobile](#/tab/mobile)
|
||||
|
||||
1. Press the  button on a note
|
||||
2. Tap `Remind me` to open the add reminder dialog
|
||||
3. Enter a title for the reminder and description (optional)
|
||||
4. Choose your reminder type:
|
||||
- **Once**: Set a specific date and time
|
||||
- **Repeat** (paid plans only): Create a recurring reminder (daily, weekly, monthly, or yearly)
|
||||
- **Permanent** _(Android only)_: A persistent reminder that shows up every day
|
||||
5. Select your notification preference (Silent, Vibrate, or Urgent)
|
||||
6. Tap the checkmark button on top right to save the reminder
|
||||
|
||||
---
|
||||
|
||||
## Creating a standalone reminder
|
||||
|
||||
You can also create reminders without attaching them to a specific note. This is useful for general tasks or events.
|
||||
|
||||
# [Desktop/Web](#/tab/web)
|
||||
|
||||
1. Go to the `Reminders` screen from the side menu
|
||||
2. Click the add reminder button () on top right
|
||||
3. Enter a title for the reminder and description (optional)
|
||||
4. Choose your reminder type (Once or Repeat)
|
||||
5. Select your notification preference (Silent, Vibrate, or Urgent)
|
||||
6. Click `Add` to save the reminder
|
||||
|
||||
# [Mobile](#/tab/mobile)
|
||||
|
||||
1. Go to the `Reminders` screen from the side menu
|
||||
2. Tap the add reminder button () on bottom right
|
||||
3. Enter a title for the reminder and description (optional)
|
||||
4. Choose your reminder type (Once, Repeat, or Permanent on Android)
|
||||
5. Select your notification preference (Silent, Vibrate, or Urgent)
|
||||
6. Tap the checkmark button on top right to save the reminder
|
||||
|
||||
---
|
||||
|
||||
## Configuring recurring reminders
|
||||
|
||||
> info Recurring reminders are a paid feature
|
||||
>
|
||||
> Recurring reminders can only be used if you are on a paid plan.
|
||||
|
||||
When you select **Repeat** mode, you can customize how often you want to be reminded:
|
||||
|
||||
- **Daily**: Get reminded every single day at your chosen time
|
||||
- **Weekly**: Select specific days of the week (e.g., weekdays only)
|
||||
- **Monthly**: Pick specific dates of the month
|
||||
- **Yearly**: Choose the month and day for your yearly reminder
|
||||
|
||||
---
|
||||
|
||||
## Notification preferences
|
||||
|
||||
Choose how you want to be notified:
|
||||
|
||||
- **Silent**: Receive a notification without sound or vibration
|
||||
- **Vibrate**: Get a notification with vibration
|
||||
- **Urgent**: Receive a notification with sound and vibration
|
||||
|
||||
> info
|
||||
>
|
||||
> Reminders require notification permissions. You'll be asked to enable notifications when you create your first reminder.
|
||||
|
||||
---
|
||||
|
||||
## Editing or viewing reminders
|
||||
|
||||
To edit an existing reminder, go to `Reminders` screen from the side menu and select the reminder you want to edit. You can change all reminder settings, including the date, time, and recurrence pattern.
|
||||
|
||||
To see all reminders attached to a note, open the note properties go to `Reminders`. This will show you a list of all reminders associated with that note.
|
||||
|
||||
---
|
||||
|
||||
## Permanent reminders (Android only)
|
||||
|
||||
**Permanent reminders** are available only on Android devices. Unlike one-time or recurring reminders, permanent reminders persist every day and won't disappear after triggering. This is useful for important daily tasks or notes you want constant access to.
|
||||
@@ -40,7 +40,7 @@ Go to `Settings` > `Editor` > `Title format` to customize the title formatting.
|
||||
|
||||
You can use a combination of above templates in the note title. For example `Note $count$ - $date$` will become `Note 150 - 06-22-2023`.
|
||||
|
||||
**$headline$**: Up to first 10 words of the note's headline. This will keep updating the title as headline of the note changes until you manually edit the title. Shouldn't be used in combination with other templates.
|
||||
**$headline$**: Up to first 60 characters of the note's first paragraph or heading. This will keep updating the title as headline of the note changes until you manually edit the title. Shouldn't be used in combination with other templates.
|
||||
|
||||
## Paragraph spacing
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ navigation:
|
||||
- path: keyboard-shortcuts.md
|
||||
|
||||
- path: privacy-mode.md
|
||||
- path: reminders.md
|
||||
- path: web-clipper
|
||||
children:
|
||||
- path: web-clipper/installation.md
|
||||
|
||||
@@ -163,13 +163,13 @@ export class TabSessionHistory {
|
||||
|
||||
canGoBack(id: string) {
|
||||
const tabHistory = this.options.get().tabSessionHistory[id];
|
||||
if (!tabHistory) return false;
|
||||
if (!tabHistory || !tabHistory.backStack) return false;
|
||||
return tabHistory.backStack.length > 1;
|
||||
}
|
||||
|
||||
canGoForward(id: string) {
|
||||
const tabHistory = this.options.get().tabSessionHistory[id];
|
||||
if (!tabHistory) return false;
|
||||
if (!tabHistory || !tabHistory.forwardStack) return false;
|
||||
return tabHistory.forwardStack.length >= 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,23 @@ test("search reminders", () =>
|
||||
}));
|
||||
|
||||
describe("notesWithHighlighting", () => {
|
||||
test("search notes with nbsp in should decode html entities", () =>
|
||||
noteTest({
|
||||
title: "(with nbsp)",
|
||||
content: {
|
||||
type: "tiptap",
|
||||
data: "<p>hello i am a note</p>"
|
||||
}
|
||||
}).then(async ({ db }) => {
|
||||
const filtered = await db.lookup.notesWithHighlighting(
|
||||
"hello",
|
||||
db.notes.all
|
||||
);
|
||||
const item = await filtered.item(0);
|
||||
expect(item.item).toBeDefined();
|
||||
expect(item.item.content[0][0].suffix.includes(" ")).toBe(false);
|
||||
}));
|
||||
|
||||
test("search notes with parentheses in query should load the item", () =>
|
||||
noteTest({
|
||||
title: "(with parantheses)"
|
||||
|
||||
@@ -186,14 +186,15 @@ test("note title with headline format should keep generating headline title unti
|
||||
|
||||
[
|
||||
["simple p tag", "<p>headline</p>", "headline"],
|
||||
["across multiple tags", "<h1>title<h1><ol><li>list</li></ol>", "titlelist"],
|
||||
["across multiple tags", "<h1>title</h1><ol><li>list</li></ol>", "title"],
|
||||
["empty first p tag", "<p></p><p>actual title</p>", "actual title"],
|
||||
[
|
||||
"content with exceeded HEADLINE_CHARACTER_LIMIT",
|
||||
"<p><strong>head</strong><em>line</em></p><h1>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam rutrum ex ac eros egestas, ut rhoncus felis faucibus. Mauris tempor orci nisl, vitae pulvinar turpis convallis n</h1>",
|
||||
"headlineLorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam rutrum ex ac eros egestas, ut rhoncus felis faucibus. Mauris tempor orci nisl,"
|
||||
"<p><strong>head</strong><em>line</em>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam rutrum ex ac eros egestas, ut rhoncus felis faucibus. Mauris tempor orci nisl, vitae pulvinar turpis convallis n</p>",
|
||||
"headlineLorem ipsum dolor sit amet, consectetur adipiscing e"
|
||||
]
|
||||
].forEach(([testCase, content, expectedHeadline]) => {
|
||||
test(`note title with headline format should generate headline title up to 150 characters - ${testCase}`, () =>
|
||||
test(`note title with headline format should generate headline title up to 60 characters - ${testCase}`, () =>
|
||||
noteTest().then(async ({ db }) => {
|
||||
await db.settings.setTitleFormat("$headline$");
|
||||
const id = await db.notes.add({
|
||||
|
||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { match } from "fuzzyjs";
|
||||
import { decodeHTML } from "entities";
|
||||
import Database from "./index.js";
|
||||
import {
|
||||
HighlightedResult,
|
||||
@@ -815,7 +816,7 @@ export function splitHighlightedMatch(text: string): Match[][] {
|
||||
}
|
||||
|
||||
matches.push({
|
||||
match,
|
||||
match: match,
|
||||
prefix: prefix.replace(/\s{2,}/gm, " ").trimStart(),
|
||||
suffix: suffix || "",
|
||||
id: matchId || undefined
|
||||
@@ -1053,7 +1054,7 @@ function highlightHtmlContent(html: string, queries: string[]): string {
|
||||
}
|
||||
},
|
||||
{
|
||||
decodeEntities: false,
|
||||
decodeEntities: true,
|
||||
xmlMode: false
|
||||
}
|
||||
);
|
||||
|
||||
@@ -94,7 +94,7 @@ export class Tiptap {
|
||||
}
|
||||
|
||||
toTitle() {
|
||||
return extractTitle(this.data, 150);
|
||||
return extractTitle(this.data, 60);
|
||||
}
|
||||
|
||||
// isEmpty() {
|
||||
|
||||
@@ -98,20 +98,35 @@ describe("lookup.fuzzy", () => {
|
||||
const items = [
|
||||
{ id: "1", title: "file search.jpg" },
|
||||
{ id: "2", title: "file-search.jpg" },
|
||||
{ id: "3", title: "file_search.jpg" }
|
||||
{ id: "3", title: "file_search.jpg" },
|
||||
{ id: "4", title: "file____search-393.jpg" },
|
||||
{ id: "5", title: "note-393.jpg" }
|
||||
];
|
||||
|
||||
test("query with space matches all separator variants", () => {
|
||||
const result = fuzzy("file search", items, (i) => i.id, { title: 1 });
|
||||
expect(result).toStrictEqual(items);
|
||||
const result = fuzzy("fl srch", items, (i) => i.id, { title: 1 });
|
||||
expect(result).toStrictEqual(items.slice(0, 4));
|
||||
});
|
||||
|
||||
test("variants with only separators should match", () => {
|
||||
const result = fuzzy(
|
||||
"---",
|
||||
[
|
||||
{ id: "1", title: "--------.jpg" },
|
||||
{ id: "2", title: "abc.jpg" }
|
||||
],
|
||||
(i) => i.id,
|
||||
{ title: 1 }
|
||||
);
|
||||
expect(result).toStrictEqual([{ id: "1", title: "--------.jpg" }]);
|
||||
});
|
||||
|
||||
test("query with special character between words matches all separator variants", () => {
|
||||
let result = fuzzy("file_search", items, (i) => i.id, { title: 1 });
|
||||
expect(result).toStrictEqual(items);
|
||||
expect(result).toStrictEqual([items[2], items[3], items[0], items[1]]);
|
||||
|
||||
result = fuzzy("file-search", items, (i) => i.id, { title: 1 });
|
||||
expect(result).toStrictEqual(items);
|
||||
expect(result).toStrictEqual([items[1], items[0], items[2], items[3]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,18 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { match, surround } from "fuzzyjs";
|
||||
import { clone } from "./clone.js";
|
||||
|
||||
const SEPARATORS = /(?<=\w)[^a-zA-Z0-9](?=\w)/g;
|
||||
|
||||
function normalizeSeparators(str: string): string {
|
||||
return str.replace(SEPARATORS, " ");
|
||||
}
|
||||
|
||||
function hasSeparator(str: string): boolean {
|
||||
const result = SEPARATORS.test(str);
|
||||
SEPARATORS.lastIndex = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function fuzzy<T>(
|
||||
query: string,
|
||||
items: T[],
|
||||
@@ -43,6 +31,50 @@ export function fuzzy<T>(
|
||||
suffix?: string;
|
||||
} = {}
|
||||
): T[] {
|
||||
const results = fuzzyMatch(query, items, getIdentifier, fields, options);
|
||||
if (results.size === 0) return [];
|
||||
|
||||
// if the query contains spaces & other non-alphanumeric characters, then we
|
||||
// should search again with a sanitized query to catch more matches.
|
||||
const sanitizedQuery = sanitize(query);
|
||||
if (sanitizedQuery !== query) {
|
||||
for (const [key, value] of fuzzyMatch(
|
||||
sanitizedQuery,
|
||||
items,
|
||||
getIdentifier,
|
||||
fields,
|
||||
options
|
||||
)) {
|
||||
const matchInFirstSearch = results.get(key);
|
||||
if (matchInFirstSearch) {
|
||||
// just give it an extra bump because matches from first search
|
||||
// must always be higher as they probably match much more closely
|
||||
// with the query given by the user.
|
||||
matchInFirstSearch.score += value.score;
|
||||
continue;
|
||||
}
|
||||
results.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const matches = Array.from(results.entries())
|
||||
.sort((a, b) => b[1].score - a[1].score)
|
||||
.map((item) => item[1].item);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function fuzzyMatch<T>(
|
||||
query: string,
|
||||
items: T[],
|
||||
getIdentifier: (item: T) => string,
|
||||
fields: Partial<Record<keyof T, number>>,
|
||||
options: {
|
||||
limit?: number;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
} = {}
|
||||
) {
|
||||
const results: Map<
|
||||
string,
|
||||
{
|
||||
@@ -51,9 +83,6 @@ export function fuzzy<T>(
|
||||
}
|
||||
> = new Map();
|
||||
|
||||
const shouldNormalize = hasSeparator(query);
|
||||
const finalQuery = shouldNormalize ? normalizeSeparators(query) : query;
|
||||
|
||||
for (const item of items) {
|
||||
if (options.limit && results.size >= options.limit) break;
|
||||
|
||||
@@ -61,10 +90,7 @@ export function fuzzy<T>(
|
||||
|
||||
for (const field in fields) {
|
||||
const value = `${item[field]}`;
|
||||
const result = match(
|
||||
finalQuery,
|
||||
shouldNormalize ? normalizeSeparators(value) : value
|
||||
);
|
||||
const result = match(query, value);
|
||||
if (!result.match) continue;
|
||||
|
||||
const oldMatch = results.get(identifier);
|
||||
@@ -87,11 +113,10 @@ export function fuzzy<T>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (results.size === 0) return [];
|
||||
|
||||
const sorted = Array.from(results.entries());
|
||||
sorted.sort((a, b) => b[1].score - a[1].score);
|
||||
|
||||
return sorted.map((item) => item[1].item);
|
||||
return results;
|
||||
}
|
||||
|
||||
const SEPARATORS = /[^a-zA-Z0-9]+/g;
|
||||
function sanitize(str: string): string {
|
||||
return str.replace(SEPARATORS, "").trim() || str;
|
||||
}
|
||||
|
||||
@@ -72,11 +72,31 @@ export function extractHeadline(html: string) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const TITLE_SOURCE_TAGS = ["p", "h1", "h2", "h3", "h4", "h5", "h6"];
|
||||
|
||||
export function extractTitle(html: string, characterLimit: number) {
|
||||
let text = "";
|
||||
let rootTag: string | undefined = undefined;
|
||||
const parser = new Parser(
|
||||
{
|
||||
onopentag: (name) => {
|
||||
if (!rootTag && TITLE_SOURCE_TAGS.includes(name)) {
|
||||
rootTag = name;
|
||||
}
|
||||
},
|
||||
onclosetag: (name) => {
|
||||
if (name === rootTag) {
|
||||
if (text) {
|
||||
parser.pause();
|
||||
parser.end();
|
||||
} else {
|
||||
rootTag = undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
ontext: (data) => {
|
||||
if (!rootTag) return;
|
||||
|
||||
text += data;
|
||||
if (text.length > characterLimit) {
|
||||
text = text.slice(0, characterLimit);
|
||||
@@ -205,7 +225,7 @@ export function extractMatchingBlocks(html: string, matchTagName: string) {
|
||||
},
|
||||
{
|
||||
lowerCaseTags: false,
|
||||
decodeEntities: false
|
||||
decodeEntities: true
|
||||
}
|
||||
);
|
||||
parser.end(html);
|
||||
|
||||
2
packages/editor-mobile/package-lock.json
generated
2
packages/editor-mobile/package-lock.json
generated
@@ -68,7 +68,7 @@
|
||||
"@notesnook/intl": "file:../intl",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
"@notesnook/ui": "file:../ui",
|
||||
"@social-embed/lib": "^0.1.0-next.7",
|
||||
"@social-embed/lib": "^0.1.0-next.11",
|
||||
"@tiptap/core": "2.6.6",
|
||||
"@tiptap/extension-blockquote": "^2.6.6",
|
||||
"@tiptap/extension-bullet-list": "^2.6.6",
|
||||
|
||||
@@ -169,7 +169,7 @@ function handleMouseDown(
|
||||
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
event.target.closest(".column-resize-handle") == null
|
||||
event.target.closest(".column-resize-handle.active") == null
|
||||
) {
|
||||
console.log("No handle target");
|
||||
return false;
|
||||
@@ -225,6 +225,10 @@ function handleMouseDown(
|
||||
|
||||
function move(event: MouseEvent | TouchEvent): void {
|
||||
if (event instanceof MouseEvent && !event.which) return finish(event);
|
||||
// Prevent the page/table from scrolling while the user is dragging a
|
||||
// column resize handle on touch. The touchmove listener is registered
|
||||
// with { passive: false } below so this call is actually honoured.
|
||||
if (isTouchEvent(event)) event.preventDefault();
|
||||
const clientX = getClientX(event);
|
||||
if (clientX === null) return;
|
||||
const pluginState = columnResizingPluginKey.getState(view.state);
|
||||
@@ -240,7 +244,9 @@ function handleMouseDown(
|
||||
win.addEventListener("mousemove", move);
|
||||
win.addEventListener("touchend", finish);
|
||||
win.addEventListener("touchcancel", finish);
|
||||
win.addEventListener("touchmove", move);
|
||||
// { passive: false } is required so that event.preventDefault() inside
|
||||
// move() is honoured — modern browsers default touchmove to passive.
|
||||
win.addEventListener("touchmove", move, { passive: false });
|
||||
event.preventDefault();
|
||||
if (isTouchEvent(event)) (view as any).domObserver.disconnectSelection();
|
||||
return true;
|
||||
@@ -422,6 +428,11 @@ function createResizeHandle(active: boolean) {
|
||||
const dom = document.createElement("div");
|
||||
dom.className = "column-resize-handle";
|
||||
if (active) dom.classList.add("active");
|
||||
// Prevent the browser from treating a touch on this handle as a scroll/pan
|
||||
// gesture. touch-action is evaluated at touchstart time, before the browser
|
||||
// commits to any scroll mode, so this reliably prevents the table's
|
||||
// overflow-scrolling container from capturing the drag on iOS Safari.
|
||||
dom.style.touchAction = "none";
|
||||
dom.onmouseenter = () => {
|
||||
dom.classList.add("active");
|
||||
};
|
||||
|
||||
@@ -215,18 +215,87 @@ export function handleMouseDown(
|
||||
// cell selection to be created).
|
||||
setCellSelection($anchor, startEvent);
|
||||
startEvent.preventDefault();
|
||||
} else if (isTouchEvent(startEvent)) {
|
||||
const selectedCell = view.domAtPos(view.state.selection.from).node;
|
||||
if (startDOMCell?.contains(selectedCell)) {
|
||||
$anchor = cellUnderMouse(view, startEvent);
|
||||
if (!$anchor) return;
|
||||
setCellSelection($anchor, startEvent);
|
||||
}
|
||||
} else if (!startDOMCell) {
|
||||
// Not in a cell, let the default behavior happen.
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTouchEvent(startEvent)) {
|
||||
// For touch, only begin tracking cross-cell selection if the touch started
|
||||
// inside the cell that already holds the active cursor/selection. Any other
|
||||
// touch is a scroll or plain tap — leave it alone.
|
||||
// Use ProseMirror cell positions rather than DOM containment so the check
|
||||
// is robust regardless of what domAtPos returns (text node, <p>, etc.).
|
||||
const $selectionCell = cellAround(view.state.selection.$anchor);
|
||||
const $touchedCell = cellUnderMouse(view, startEvent);
|
||||
if (
|
||||
!$selectionCell ||
|
||||
!$touchedCell ||
|
||||
$selectionCell.pos !== $touchedCell.pos ||
|
||||
!view.state.selection.empty
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-scroll state for touch cell selection. While the user is dragging a
|
||||
// cell selection, normal scroll is suppressed (event.preventDefault). Instead
|
||||
// we watch how close the finger is to the edges of the nearest scrollable
|
||||
// ancestor and scroll that container programmatically.
|
||||
const SCROLL_THRESHOLD = 40; // px from edge before scroll kicks in
|
||||
const SCROLL_MAX_SPEED = 4; // px per animation frame at the very edge
|
||||
let autoScrollRAF: number | null = null;
|
||||
let autoScrollX = 0;
|
||||
let autoScrollY = 0;
|
||||
let scrollTarget: Element | null = null;
|
||||
|
||||
function scrollStep(): void {
|
||||
autoScrollRAF = null;
|
||||
if (!scrollTarget) return;
|
||||
const rect = scrollTarget.getBoundingClientRect();
|
||||
const leftDist = autoScrollX - rect.left;
|
||||
const rightDist = rect.right - autoScrollX;
|
||||
const topDist = autoScrollY - rect.top;
|
||||
const bottomDist = rect.bottom - autoScrollY;
|
||||
const dx =
|
||||
leftDist < SCROLL_THRESHOLD
|
||||
? -SCROLL_MAX_SPEED * (1 - leftDist / SCROLL_THRESHOLD)
|
||||
: rightDist < SCROLL_THRESHOLD
|
||||
? SCROLL_MAX_SPEED * (1 - rightDist / SCROLL_THRESHOLD)
|
||||
: 0;
|
||||
const dy =
|
||||
topDist < SCROLL_THRESHOLD
|
||||
? -SCROLL_MAX_SPEED * (1 - topDist / SCROLL_THRESHOLD)
|
||||
: bottomDist < SCROLL_THRESHOLD
|
||||
? SCROLL_MAX_SPEED * (1 - bottomDist / SCROLL_THRESHOLD)
|
||||
: 0;
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
scrollTarget.scrollBy(dx, dy);
|
||||
autoScrollRAF = requestAnimationFrame(scrollStep);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAutoScroll(x: number, y: number): void {
|
||||
autoScrollX = x;
|
||||
autoScrollY = y;
|
||||
if (!scrollTarget)
|
||||
// Walk up from the starting cell, not from view.dom. The table's scroll
|
||||
// container (.scroll-bar) is an ancestor of the <td>, whereas view.dom is
|
||||
// the editor root which sits *inside* the scroll container — walking up
|
||||
// from it would skip past the table scroller and find the page scroller.
|
||||
scrollTarget = findScrollableAncestor(startDOMCell ?? view.dom);
|
||||
// Only schedule a new frame if one isn't already pending.
|
||||
if (autoScrollRAF == null)
|
||||
autoScrollRAF = requestAnimationFrame(scrollStep);
|
||||
}
|
||||
|
||||
function stopAutoScroll(): void {
|
||||
if (autoScrollRAF != null) {
|
||||
cancelAnimationFrame(autoScrollRAF);
|
||||
autoScrollRAF = null;
|
||||
}
|
||||
scrollTarget = null;
|
||||
}
|
||||
|
||||
// Create and dispatch a cell selection between the given anchor and
|
||||
// the position under the mouse.
|
||||
function setCellSelection(
|
||||
@@ -248,39 +317,88 @@ export function handleMouseDown(
|
||||
}
|
||||
|
||||
// Stop listening to mouse motion events.
|
||||
function stop(): void {
|
||||
function stop(event?: Event): void {
|
||||
view.root.removeEventListener("mouseup", stop);
|
||||
view.root.removeEventListener("dragstart", stop);
|
||||
view.root.removeEventListener("mousemove", move);
|
||||
view.root.removeEventListener("touchmove", move);
|
||||
view.root.removeEventListener("touchend", stop);
|
||||
view.root.removeEventListener("touchcancel", stop);
|
||||
if (tableEditingKey.getState(view.state) != null) {
|
||||
const hadActiveSelection = tableEditingKey.getState(view.state) != null;
|
||||
stopAutoScroll();
|
||||
if (hadActiveSelection) {
|
||||
(view as any).domObserver.suppressSelectionUpdates();
|
||||
view.dispatch(view.state.tr.setMeta(tableEditingKey, -1));
|
||||
}
|
||||
// Prevent native drag-and-drop from moving/deleting selected text when the
|
||||
// browser interprets a cell-selection drag as a text drag (Bug: content
|
||||
// becomes empty). Also prevent the browser's synthetic tap after a
|
||||
// touch-based cell selection from collapsing the CellSelection back to a
|
||||
// text cursor in the last touched cell (Bug: cell selection disappears).
|
||||
if (
|
||||
event &&
|
||||
(event.type === "dragstart" ||
|
||||
(event.type === "touchend" && hadActiveSelection))
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function move(_event: Event): void {
|
||||
const event = _event as MouseEvent | DragEvent | TouchEvent;
|
||||
|
||||
// For touch: we've already committed this gesture to cell-selection tracking
|
||||
// (the guard at touchstart ensured that). Suppress the browser scroll on
|
||||
// every touchmove — this must happen here because:
|
||||
// a) touchstart preventDefault is ignored when registered as passive (the
|
||||
// default on most mobile browsers for ProseMirror's event dispatch), and
|
||||
// b) our own touchmove listener is registered with { passive: false } so
|
||||
// preventDefault() here is guaranteed to work.
|
||||
if (isTouchEvent(event)) {
|
||||
event.preventDefault();
|
||||
// Always feed the current finger position to auto-scroll regardless of
|
||||
// whether a cross-cell selection has started yet. This must be outside the
|
||||
// branch below because once tableEditingKey has state (anchor != null) we
|
||||
// no longer enter the isTouchEvent branch, so coordinates would go stale.
|
||||
const x = getClientX(event);
|
||||
const y = getClientY(event);
|
||||
if (x != null && y != null) updateAutoScroll(x, y);
|
||||
}
|
||||
|
||||
const anchor = tableEditingKey.getState(view.state);
|
||||
let $anchor;
|
||||
if (anchor != null) {
|
||||
// Continuing an existing cross-cell selection
|
||||
$anchor = view.state.doc.resolve(anchor);
|
||||
} else if (isTouchEvent(event)) {
|
||||
// For touch events, event.target stays fixed at the touchstart element,
|
||||
// so use coordinates to detect movement into a different cell.
|
||||
const $startCell = cellUnderMouse(view, startEvent);
|
||||
const $currentCell = cellUnderMouse(view, event);
|
||||
if ($startCell && $currentCell && $startCell.pos !== $currentCell.pos) {
|
||||
$anchor = $startCell;
|
||||
}
|
||||
} else if (domInCell(view, event.target as Node) != startDOMCell) {
|
||||
// Moving out of the initial cell -- start a new cell selection
|
||||
$anchor = cellUnderMouse(view, startEvent);
|
||||
if (!$anchor) return stop();
|
||||
}
|
||||
if ($anchor) setCellSelection($anchor, event);
|
||||
if ($anchor) {
|
||||
// For mouse: prevent native drag-and-drop from interfering.
|
||||
if (!isTouchEvent(event)) event.preventDefault();
|
||||
setCellSelection($anchor, event);
|
||||
}
|
||||
}
|
||||
|
||||
view.root.addEventListener("mouseup", stop);
|
||||
view.root.addEventListener("dragstart", stop);
|
||||
view.root.addEventListener("mousemove", move);
|
||||
view.root.addEventListener("touchmove", move);
|
||||
view.root.addEventListener("touchend", stop);
|
||||
// { passive: false } is required for touchmove/touchend so that
|
||||
// event.preventDefault() inside the handlers actually suppresses scroll.
|
||||
// Without it, modern browsers treat the listener as passive by default and
|
||||
// silently ignore any preventDefault() calls.
|
||||
view.root.addEventListener("touchmove", move, { passive: false });
|
||||
view.root.addEventListener("touchend", stop, { passive: false });
|
||||
view.root.addEventListener("touchcancel", stop);
|
||||
}
|
||||
|
||||
@@ -330,3 +448,11 @@ function cellUnderMouse(
|
||||
if (!mousePos) return null;
|
||||
return mousePos ? cellAround(view.state.doc.resolve(mousePos.pos)) : null;
|
||||
}
|
||||
|
||||
function findScrollableAncestor(el: HTMLElement | Node): Element {
|
||||
return (
|
||||
el.parentElement?.closest(".scroll-bar") ||
|
||||
document.scrollingElement ||
|
||||
document.documentElement
|
||||
);
|
||||
}
|
||||
|
||||
@@ -831,10 +831,6 @@ msgstr "All tools in this group will be removed from the toolbar."
|
||||
msgid "All your backups are stored in 'Phone Storage/Notesnook/backups/' folder"
|
||||
msgstr "All your backups are stored in 'Phone Storage/Notesnook/backups/' folder"
|
||||
|
||||
#: src/strings.ts:1078
|
||||
msgid "All your data will be removed permanently. Make sure you have saved backup of your notes. This action is IRREVERSIBLE."
|
||||
msgstr "All your data will be removed permanently. Make sure you have saved backup of your notes. This action is IRREVERSIBLE."
|
||||
|
||||
#: src/strings.ts:108
|
||||
msgid "Already have an account?"
|
||||
msgstr "Already have an account?"
|
||||
@@ -1109,6 +1105,10 @@ msgstr "Available on iOS"
|
||||
msgid "Available on iOS & Android"
|
||||
msgstr "Available on iOS & Android"
|
||||
|
||||
#: src/strings.ts:2640
|
||||
msgid "Back"
|
||||
msgstr "Back"
|
||||
|
||||
#: src/strings.ts:2302
|
||||
msgid "Background color"
|
||||
msgstr "Background color"
|
||||
@@ -3448,6 +3448,10 @@ msgstr "Invalid CORS proxy url"
|
||||
msgid "Invalid email"
|
||||
msgstr "Invalid email"
|
||||
|
||||
#: src/strings.ts:2642
|
||||
msgid "Invalid recovery key. Make sure to input your account recovery key, not a 2FA recovery code."
|
||||
msgstr "Invalid recovery key. Make sure to input your account recovery key, not a 2FA recovery code."
|
||||
|
||||
#: src/strings.ts:224
|
||||
msgid "Issue created"
|
||||
msgstr "Issue created"
|
||||
@@ -7489,6 +7493,10 @@ msgstr "Your account password must be strong & unique."
|
||||
msgid "Your account will be downgraded in {days} days"
|
||||
msgstr "Your account will be downgraded in {days} days"
|
||||
|
||||
#: src/strings.ts:1078
|
||||
msgid "Your account will be permanently deleted along with all your data, login credentials, and subscription information. This action is IRREVERSIBLE. Make sure you have saved a backup of your notes before proceeding."
|
||||
msgstr "Your account will be permanently deleted along with all your data, login credentials, and subscription information. This action is IRREVERSIBLE. Make sure you have saved a backup of your notes before proceeding."
|
||||
|
||||
#: src/strings.ts:960
|
||||
msgid "Your archive"
|
||||
msgstr "Your archive"
|
||||
|
||||
@@ -831,10 +831,6 @@ msgstr ""
|
||||
msgid "All your backups are stored in 'Phone Storage/Notesnook/backups/' folder"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1078
|
||||
msgid "All your data will be removed permanently. Make sure you have saved backup of your notes. This action is IRREVERSIBLE."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:108
|
||||
msgid "Already have an account?"
|
||||
msgstr ""
|
||||
@@ -1109,6 +1105,10 @@ msgstr ""
|
||||
msgid "Available on iOS & Android"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2640
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2302
|
||||
msgid "Background color"
|
||||
msgstr ""
|
||||
@@ -3428,6 +3428,10 @@ msgstr ""
|
||||
msgid "Invalid email"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2642
|
||||
msgid "Invalid recovery key. Make sure to input your account recovery key, not a 2FA recovery code."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:224
|
||||
msgid "Issue created"
|
||||
msgstr ""
|
||||
@@ -7434,6 +7438,10 @@ msgstr ""
|
||||
msgid "Your account will be downgraded in {days} days"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1078
|
||||
msgid "Your account will be permanently deleted along with all your data, login credentials, and subscription information. This action is IRREVERSIBLE. Make sure you have saved a backup of your notes before proceeding."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:960
|
||||
msgid "Your archive"
|
||||
msgstr ""
|
||||
|
||||
@@ -1075,7 +1075,7 @@ $day$: Current day (eg. Monday)`,
|
||||
logoutError: () => t`Error logging out`,
|
||||
deleteAccount: () => t`Delete account`,
|
||||
deleteAccountDesc: () =>
|
||||
t`All your data will be removed permanently. Make sure you have saved backup of your notes. This action is IRREVERSIBLE.`,
|
||||
t`Your account will be permanently deleted along with all your data, login credentials, and subscription information. This action is IRREVERSIBLE. Make sure you have saved a backup of your notes before proceeding.`,
|
||||
enterAccountPassword: () => t`Enter account password`,
|
||||
enterAccountPasswordDesc: () => t`Enter account password to proceed.`,
|
||||
failedToDeleteAccount: () => t`Failed to delete account`,
|
||||
@@ -2636,5 +2636,8 @@ Use this if changes from other devices are not appearing on this device. This wi
|
||||
deleteData: () => t`Delete data`,
|
||||
failedToAttachFile: () => t`Failed to attach file`,
|
||||
unlockNoteToMergeConflicts: () => t`Unlock note to merge conflicts`,
|
||||
confirmationEmailSent: () => t`Confirmation email sent`
|
||||
confirmationEmailSent: () => t`Confirmation email sent`,
|
||||
back: () => t`Back`,
|
||||
invalidRecoveryKey: () =>
|
||||
t`Invalid recovery key. Make sure to input your account recovery key, not a 2FA recovery code.`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user