Merge branch 'master' into beta

This commit is contained in:
Ammar Ahmed
2026-02-16 14:40:45 +05:00
32 changed files with 213 additions and 147 deletions

View File

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

View File

@@ -1,13 +1,3 @@
- Add notebooks, tags and colors to Home Screen Shortcuts
- Change day format and use /day in notes
- Add Setting to change default editor line height
- Set a custom title for monographs
- Add webpage title and date clipped to web clips
- Configure Week to start from Sunday or Monday
- Change Note's creation date
- Set expiry date on notes
- Temporarily disable password change and recovery options
- Note history now includes note title
- Minor bug fixes
- Bug fixes and minor improvements
Thank you for using Notesnook!

View File

@@ -167,14 +167,16 @@ const ColorPicker = ({
);
if (!title.current)
return ToastManager.error(
new Error(strings.allFieldsRequired())
new Error(strings.allFieldsRequired()),
"color-picker"
);
const exists = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", selectedColor)])
);
if (exists)
return ToastManager.error(
new Error(strings.colorExists(selectedColor))
new Error(strings.colorExists(selectedColor)),
"color-picker"
);
const id = await db.colors.add({
title: title.current,

View File

@@ -76,6 +76,7 @@ import { IconButton } from "../ui/icon-button";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { db } from "../../common/database";
const Steps = {
select: 1,
@@ -139,7 +140,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
}, [isFocused, step]);
useEffect(() => {
const sub = EV.subscribe(
const sub = db.eventManager.subscribe(
EVENTS.userSubscriptionUpdated,
(sub: User["subscription"]) => {
if (sub.plan === SubscriptionPlan.FREE) return;
@@ -1007,8 +1008,8 @@ const PricingPlanCard = ({
: "monthly"
}`
: pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: (product?.productId as string)
? (WebPlan?.period as string)
: (product?.productId as string)
);
setStep(Steps.buy);
}}

View File

@@ -724,24 +724,39 @@ export const useAppEvents = () => {
useEffect(() => {
const subscriptions = [
EV.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
EV.subscribe(EVENTS.syncAborted, onSyncAborted),
EV.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
db.eventManager.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
db.eventManager.subscribe(EVENTS.syncAborted, onSyncAborted),
db.eventManager.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
db.eventManager.subscribe(EVENTS.userLoggedOut, onLogout),
db.eventManager.subscribe(EVENTS.userEmailConfirmed, onUserEmailVerified),
EV.subscribe(EVENTS.userSessionExpired, onUserSessionExpired),
db.eventManager.subscribe(
EVENTS.userSessionExpired,
onUserSessionExpired
),
db.eventManager.subscribe(
EVENTS.userSubscriptionUpdated,
onUserSubscriptionStatusChanged
),
EV.subscribe(EVENTS.fileDownload, onDownloadingAttachmentProgress),
EV.subscribe(EVENTS.fileUpload, onUploadingAttachmentProgress),
EV.subscribe(EVENTS.fileDownloaded, onDownloadedAttachmentProgress),
EV.subscribe(EVENTS.fileUploaded, onUploadedAttachmentProgress),
EV.subscribe(EVENTS.downloadCanceled, (data) => {
db.eventManager.subscribe(
EVENTS.fileDownload,
onDownloadingAttachmentProgress
),
db.eventManager.subscribe(
EVENTS.fileUpload,
onUploadingAttachmentProgress
),
db.eventManager.subscribe(
EVENTS.fileDownloaded,
onDownloadedAttachmentProgress
),
db.eventManager.subscribe(
EVENTS.fileUploaded,
onUploadedAttachmentProgress
),
db.eventManager.subscribe(EVENTS.downloadCanceled, (data) => {
useAttachmentStore.getState().setDownloading(data);
}),
EV.subscribe(EVENTS.uploadCanceled, (data) => {
db.eventManager.subscribe(EVENTS.uploadCanceled, (data) => {
useAttachmentStore.getState().setUploading(data);
}),
EV.subscribe(EVENTS.migrationStarted, (name) => {
@@ -767,7 +782,7 @@ export const useAppEvents = () => {
return;
endProgress();
}),
EV.subscribe(EVENTS.vaultLocked, async () => {
db.eventManager.subscribe(EVENTS.vaultLocked, async () => {
// Lock all notes in all tabs...
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
@@ -799,7 +814,6 @@ export const useAppEvents = () => {
return () => {
emitterSubscriptions.forEach((sub) => sub?.remove?.());
subscriptions.forEach((sub) => sub?.unsubscribe?.());
EV.unsubscribeAll();
};
}, [onSyncComplete, onUserUpdated]);

View File

@@ -29,7 +29,6 @@ export type SyncProgressEventType = {
const useSyncProgress = () => {
const [progress, setProgress] = useState<SyncProgressEventType>();
const EV = db.eventManager;
const onProgress = useCallback(
({ type, current, total }: SyncProgressEventType) => {
@@ -42,13 +41,13 @@ const useSyncProgress = () => {
setProgress(undefined);
};
useEffect(() => {
EV?.subscribe(EVENTS.syncProgress, onProgress);
EV?.subscribe(EVENTS.syncCompleted, onSyncComplete);
db.eventManager.subscribe(EVENTS.syncProgress, onProgress);
db.eventManager.subscribe(EVENTS.syncCompleted, onSyncComplete);
return () => {
EV?.unsubscribe(EVENTS.syncProgress, onProgress);
EV?.unsubscribe(EVENTS.syncCompleted, onSyncComplete);
db.eventManager.unsubscribe(EVENTS.syncProgress, onProgress);
db.eventManager.unsubscribe(EVENTS.syncCompleted, onSyncComplete);
};
}, [EV, onProgress]);
}, [onProgress]);
return {
progress

View File

@@ -25,7 +25,8 @@ import React, {
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef
useRef,
useState
} from "react";
import { Platform, ViewStyle } from "react-native";
import WebView from "react-native-webview";
@@ -109,17 +110,20 @@ const Editor = React.memo(
noToolbar,
noHeader
});
const renderKey = useRef(randId("editor-id") + editorId);
const [renderKey, setRenderKey] = useState(
randId("editor-id") + editorId
);
useImperativeHandle(ref, () => ({
get: () => editor
}));
useLockedNoteHandler();
const onError = useCallback(() => {
renderKey.current = randId("editor-id") + editorId;
setRenderKey(randId("editor-id") + editorId);
editor.state.current.ready = false;
editor.state.current.initialLoadCalled = false;
editor.setLoading(true);
}, [editor, editorId]);
}, [editor]);
useEffect(() => {
const sub = [eSubscribeEvent(eEditorReset, onError)];
@@ -144,7 +148,7 @@ const Editor = React.memo(
<WebView
testID={notesnook.editor.id}
ref={editor.ref}
key={renderKey.current}
key={renderKey}
onRenderProcessGone={onError}
nestedScrollEnabled
onError={onError}

View File

@@ -27,7 +27,7 @@ import {
KeepLocalCopyResponse,
pick as pickFile
} from "@react-native-documents/picker";
import { basename } from "pathe";
import { basename, dirname } from "pathe";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { Image, openCamera, openPicker } from "react-native-image-crop-picker";
@@ -119,7 +119,9 @@ const file = async (fileOptions: PickerOptions) => {
throw new Error("Failed to attach file");
}
await RNFetchBlob.fs.unlink(uri);
RNFetchBlob.fs.unlink(dirname(fileCopyUri.localUri)).catch((e) => {
console.log(e, "error");
});
if (
fileOptions.tabId !== undefined &&
@@ -140,6 +142,7 @@ const file = async (fileOptions: PickerOptions) => {
throw new Error("Failed to attach file, no tabId is set");
}
} catch (e) {
console.log("ERROR OCCURED HERE.", e);
ToastManager.show({
heading: (e as Error).message,
type: "error",
@@ -250,7 +253,6 @@ const handleImageResponse = async (
for (const image of response) {
const isPng = /(png)/g.test(image.mime);
const isJpeg = /(jpeg|jpg)/g.test(image.mime);
if (compress && (isPng || isJpeg)) {
image.path = await compressToFile(
Platform.OS === "ios" ? "file://" + image.path : image.path,
@@ -290,7 +292,7 @@ const handleImageResponse = async (
if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
RNFetchBlob.fs.unlink(uri).catch((e) => {});
if (
options.tabId !== undefined &&
@@ -375,9 +377,7 @@ export async function attachFile(
return true;
} catch (e) {
DatabaseLogger.error(e);
if (Platform.OS === "ios") {
await RNFetchBlob.fs.unlink(uri);
}
RNFetchBlob.fs.unlink(uri).catch((e) => {});
return false;
}
}

View File

@@ -684,8 +684,6 @@ export const useEditor = (
lastContentChangeTime.current[item.id] = item.dateEdited;
currentNotes.current[item.id] = item;
if (!currentNotes.current[item.id]) return;
editorSessionHistory.newSession(item.id);
await commands.setStatus(
@@ -1097,6 +1095,8 @@ export const useEditor = (
if (noteId) {
const note = await db.notes?.note(noteId);
fluidTabsRef.current?.goToPage("editor");
loadNoteMutex.cancel();
loadNoteMutex.release();
if (note) {
loadNote({
item: note
@@ -1104,6 +1104,8 @@ export const useEditor = (
}
} else {
noteId = useTabStore.getState().getCurrentNoteId() || null;
loadNoteMutex.cancel();
loadNoteMutex.release();
if (!noteId) {
loadNote({ newNote: true });
if (fluidTabsRef.current?.page() === "editor") {

View File

@@ -193,11 +193,13 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
headerTitle={notebook?.title}
loading={loading}
CustomLisHeader={
<NotebookHeader
breadcrumbs={breadcrumbs}
notebook={notebook!}
totalNotes={notes?.placeholders.length || 0}
/>
notebook ? (
<NotebookHeader
breadcrumbs={breadcrumbs}
notebook={notebook}
totalNotes={notes?.placeholders.length || 0}
/>
) : undefined
}
placeholder={{
title: notebook?.title!,

View File

@@ -1029,7 +1029,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2172;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1104,7 +1104,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.14;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1135,7 +1135,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2172;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1210,7 +1210,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.14;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1367,7 +1367,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2172;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1379,7 +1379,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1410,7 +1410,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2172;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1423,7 +1423,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.14;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1453,7 +1453,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2172;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1534,7 +1534,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1565,7 +1565,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2172;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1647,7 +1647,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.14;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.3.13",
"version": "3.3.14",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {
@@ -208,4 +208,4 @@
"engines": {
"node": ">=20"
}
}
}

View File

@@ -31,7 +31,7 @@ import {
} from "./common";
import { AppEventManager, AppEvents } from "./common/app-events";
import { db } from "./common/db";
import { EV, EVENTS } from "@notesnook/core";
import { EVENTS } from "@notesnook/core";
import { registerKeyMap } from "./common/key-map";
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
import { hashNavigate } from "./navigation";
@@ -113,7 +113,7 @@ export default function AppEffects() {
}
}
const fileDownloadEvents = EV.subscribeMulti(
const fileDownloadEvents = db.eventManager.subscribeMulti(
[EVENTS.fileDownloaded, EVENTS.fileDownload],
({ total, current }: { total: number; current: number }) => {
handleDownloadUploadProgress("download", total, current);
@@ -121,7 +121,7 @@ export default function AppEffects() {
null
);
const fileUploadEvents = EV.subscribeMulti(
const fileUploadEvents = db.eventManager.subscribeMulti(
[EVENTS.fileUploaded, EVENTS.fileUpload],
({ total, current }: { total: number; current: number }) => {
handleDownloadUploadProgress("upload", total, current);

View File

@@ -68,7 +68,6 @@ import { showFeatureNotAllowedToast } from "../../common/toasts";
import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog";
import { ConfirmDialog } from "../../dialogs/confirm";
import { strings } from "@notesnook/intl";
import { AppEventManager, AppEvents } from "../../common/app-events";
export type OnChangeHandler = (
content: () => string,

View File

@@ -23,7 +23,7 @@ import { Loading, Refresh } from "../icons";
import { db } from "../../common/db";
import { writeText } from "clipboard-polyfill";
import { showToast } from "../../utils/toast";
import { EV, EVENTS, hosts, MonographAnalytics } from "@notesnook/core";
import { EVENTS, hosts } from "@notesnook/core";
import { useStore } from "../../stores/monograph-store";
import { Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
@@ -67,7 +67,7 @@ function PublishView(props: PublishViewProps) {
}, [monograph?.id, monographAnalytics]);
useEffect(() => {
const fileDownloadedEvent = EV.subscribe(
const fileDownloadedEvent = db.eventManager.subscribe(
EVENTS.fileDownloaded,
({ total, current, groupId }) => {
if (!groupId || !groupId.includes(note.id)) return;

View File

@@ -1,4 +1,4 @@
import { EV, EVENTS } from "@notesnook/core";
import { EVENTS } from "@notesnook/core";
import { useEffect, useState } from "react";
import Vault from "../common/vault";
import { db } from "../common/db";
@@ -7,8 +7,8 @@ export function useVault() {
const [isLocked, setIsLocked] = useState(!db.vault.unlocked);
useEffect(() => {
EV.subscribe(EVENTS.vaultLocked, () => setIsLocked(true));
EV.subscribe(EVENTS.vaultUnlocked, () => setIsLocked(false));
db.eventManager.subscribe(EVENTS.vaultLocked, () => setIsLocked(true));
db.eventManager.subscribe(EVENTS.vaultUnlocked, () => setIsLocked(false));
}, []);
return {

View File

@@ -30,7 +30,7 @@ import { store as settingStore } from "./setting-store";
import BaseStore from "./index";
import { showToast } from "../utils/toast";
import { Notice } from "../common/notices";
import { EV, EVENTS, SYNC_CHECK_IDS, SyncOptions } from "@notesnook/core";
import { EVENTS, SYNC_CHECK_IDS, SyncOptions } from "@notesnook/core";
import { logger } from "../utils/logger";
import Config from "../utils/config";
import {
@@ -92,7 +92,7 @@ class AppStore extends BaseStore<AppStore> {
});
this.get().sync({ type: "full" });
EV.subscribe(EVENTS.appRefreshRequested, () => this.refresh());
db.eventManager.subscribe(EVENTS.appRefreshRequested, () => this.refresh());
db.eventManager.subscribe(EVENTS.syncCompleted, () => this.refresh());
db.eventManager.subscribe(EVENTS.syncProgress, ({ type, current }) => {
@@ -105,7 +105,7 @@ class AppStore extends BaseStore<AppStore> {
});
});
EV.subscribe(EVENTS.syncCheckStatus, async (type) => {
db.eventManager.subscribe(EVENTS.syncCheckStatus, async (type) => {
const { isAutoSyncEnabled, isSyncEnabled } = this.get();
switch (type) {
case SYNC_CHECK_IDS.sync:

View File

@@ -23,7 +23,7 @@ import { store as appStore } from "./app-store";
import { useStore as useSettingStore } from "./setting-store";
import { db } from "../common/db";
import BaseStore from ".";
import { EV, EVENTS } from "@notesnook/core";
import { EVENTS } from "@notesnook/core";
import { logger } from "../utils/logger";
import Config from "../utils/config";
import { setDocumentTitle } from "../utils/dom";
@@ -255,7 +255,7 @@ class EditorStore extends BaseStore<EditorStore> {
closeTabs(...tabs.map((s) => s.id));
});
EV.subscribe(EVENTS.vaultLocked, () => {
db.eventManager.subscribe(EVENTS.vaultLocked, () => {
this.set((state) => {
state.sessions = state.sessions.map((session) => {
if (isLockedSession(session)) {

View File

@@ -20,7 +20,7 @@ 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 { EV, EVENTS } from "@notesnook/core";
import { EVENTS } from "@notesnook/core";
import Config from "../utils/config";
import { hashNavigate } from "../navigation";
import { AuthenticatorType, User } from "@notesnook/core";
@@ -39,7 +39,7 @@ class UserStore extends BaseStore<UserStore> {
counter = 0;
init = () => {
EV.subscribe(EVENTS.userSessionExpired, async () => {
db.eventManager.subscribe(EVENTS.userSessionExpired, async () => {
Config.set("sessionExpired", true);
window.location.replace("/sessionexpired");
});
@@ -53,7 +53,8 @@ class UserStore extends BaseStore<UserStore> {
user,
isLoggedIn: true
});
if (Config.get("sessionExpired")) EV.publish(EVENTS.userSessionExpired);
if (Config.get("sessionExpired"))
db.eventManager.publish(EVENTS.userSessionExpired);
});
if (Config.get("sessionExpired")) return;

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import "../app.css";
import { useEffect, useState } from "react";
import { Box, Button, Flex, Text } from "@theme-ui/components";
import { hardNavigate, hashNavigate, useQueryParams } from "../navigation";
import { hardNavigate, useQueryParams } from "../navigation";
import { Support } from "../components/icons";
import { HeadlessAuth } from "./auth";
import {
@@ -39,7 +39,8 @@ import { isUserSubscribed } from "../hooks/use-is-user-premium";
import { PLAN_METADATA } from "../dialogs/buy-dialog/plans";
import { planToAvailability } from "@notesnook/common";
import { FeatureCaption } from "../dialogs/buy-dialog/feature-caption";
import { EV, EVENTS } from "@notesnook/core";
import { EVENTS } from "@notesnook/core";
import { db } from "../common/db";
export type Plan = z.infer<typeof PlanSchema>;
@@ -119,9 +120,12 @@ function Checkout() {
useEffect(() => {
if (currentStep === 2) {
const event = EV.subscribe(EVENTS.userSubscriptionUpdated, () => {
hardNavigate("/notes#/welcome");
});
const event = db.eventManager.subscribe(
EVENTS.userSubscriptionUpdated,
() => {
hardNavigate("/notes#/welcome");
}
);
return () => {
event.unsubscribe();
};

View File

@@ -0,0 +1,13 @@
- Add notebooks, tags and colors to Home Screen Shortcuts
- Change day format and use /day in notes
- Add Setting to change default editor line height
- Set a custom title for monographs
- Add webpage title and date clipped to web clips
- Configure Week to start from Sunday or Monday
- Change Note's creation date
- Set expiry date on notes
- Temporarily disable password change and recovery options
- Note history now includes note title
- Minor bug fixes
Thank you for using Notesnook!

View File

@@ -0,0 +1,3 @@
- Bug fixes and minor improvements
Thank you for using Notesnook!

View File

@@ -24,7 +24,8 @@ import {
FeatureResult,
isFeatureAvailable
} from "../utils/index.js";
import { EV, EVENTS } from "@notesnook/core";
import { EVENTS } from "@notesnook/core";
import { database } from "../database.js";
export function useIsFeatureAvailable<TId extends FeatureId>(
id: TId | undefined,
@@ -36,7 +37,7 @@ export function useIsFeatureAvailable<TId extends FeatureId>(
if (!id) return;
isFeatureAvailable(id, value).then((result) => setResult(result));
const userSubscriptionUpdated = EV.subscribe(
const userSubscriptionUpdated = database.eventManager.subscribe(
EVENTS.userSubscriptionUpdated,
() => {
isFeatureAvailable(id, value).then((result) => setResult(result));
@@ -59,7 +60,7 @@ export function useAreFeaturesAvailable<TIds extends FeatureId[]>(
useEffect(() => {
areFeaturesAvailable(ids, values).then((result) => setResult(result));
const userSubscriptionUpdated = EV.subscribe(
const userSubscriptionUpdated = database.eventManager.subscribe(
EVENTS.userSubscriptionUpdated,
() => {
areFeaturesAvailable(ids, values).then((result) => setResult(result));

View File

@@ -30,7 +30,7 @@ import Lookup from "./lookup.js";
import { Content } from "../collections/content.js";
import Backup from "../database/backup.js";
import Hosts from "../utils/constants.js";
import { EV, EVENTS } from "../common.js";
import { EVENTS } from "../common.js";
import { LegacySettings } from "../collections/legacy-settings.js";
import Migrations from "./migrations.js";
import UserManager from "./user-manager.js";
@@ -126,7 +126,11 @@ class Database {
);
return (
this._fs ||
(this._fs = new FileStorage(this.options.fs, this.tokenManager))
(this._fs = new FileStorage(
this.options.fs,
this.tokenManager,
this.eventManager
))
);
};
@@ -191,7 +195,7 @@ class Database {
options!: Options;
eventSource?: EventSource | null;
tokenManager = new TokenManager(this.kv);
tokenManager = new TokenManager(this.kv, this.eventManager);
mfa = new MFAManager(this.tokenManager);
subscriptions = new Subscriptions(this);
circle = new Circle(this);
@@ -293,10 +297,13 @@ class Database {
this.connectSSE,
this
);
EV.subscribe(EVENTS.tokenRefreshed, () => this.connectSSE());
EV.subscribe(EVENTS.attachmentDeleted, async (attachment: Attachment) => {
await this.fs().cancel(attachment.hash);
});
this.eventManager.subscribe(EVENTS.tokenRefreshed, () => this.connectSSE());
this.eventManager.subscribe(
EVENTS.attachmentDeleted,
async (attachment: Attachment) => {
await this.fs().cancel(attachment.hash);
}
);
this.eventManager.subscribe(EVENTS.userLoggedOut, async () => {
await this.monographs.clear();
await this.fs().clear();

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Cipher } from "@notesnook/crypto";
import Database from "../index.js";
import { CURRENT_DATABASE_VERSION, EV, EVENTS } from "../../common.js";
import { CURRENT_DATABASE_VERSION, EVENTS } from "../../common.js";
import { logger } from "../../logger.js";
import {
SyncItem,
@@ -49,7 +49,7 @@ class Collector {
): AsyncGenerator<SyncTransferItem, void, unknown> {
const keys = await this.db.user.getDataEncryptionKeys();
if (!keys || !keys.length) {
EV.publish(EVENTS.userSessionExpired);
this.db.eventManager.publish(EVENTS.userSessionExpired);
throw new Error("User encryption key not generated. Please relogin.");
}

View File

@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
checkSyncStatus,
CURRENT_DATABASE_VERSION,
EV,
EVENTS,
sendSyncProgressEvent,
SYNC_CHECK_IDS
@@ -104,7 +103,7 @@ export default class SyncManager {
async start(options: SyncOptions) {
try {
if (await checkSyncStatus(SYNC_CHECK_IDS.autoSync))
if (await checkSyncStatus(this.db.eventManager, SYNC_CHECK_IDS.autoSync))
await this.sync.autoSync.start();
await this.sync.start(options);
return true;
@@ -177,7 +176,7 @@ export class Sync {
await this.createConnection(options);
if (!this.connection) return;
if (!(await checkSyncStatus(SYNC_CHECK_IDS.sync))) {
if (!(await checkSyncStatus(this.db.eventManager, SYNC_CHECK_IDS.sync))) {
await this.connection.stop();
return;
}
@@ -207,7 +206,9 @@ export class Sync {
await this.stop(options);
if (!(await checkSyncStatus(SYNC_CHECK_IDS.autoSync))) {
if (
!(await checkSyncStatus(this.db.eventManager, SYNC_CHECK_IDS.autoSync))
) {
await this.connection.stop();
this.autoSync.stop();
}
@@ -454,7 +455,7 @@ export class Sync {
const { HubConnectionBuilder, HttpTransportType, JsonHubProtocol } =
await import("@microsoft/signalr");
const tokenManager = new TokenManager(this.db.kv);
const tokenManager = new TokenManager(this.db.kv, this.db.eventManager);
this.connection = new HubConnectionBuilder()
.withUrl(`${Constants.API_HOST}/hubs/sync/v2`, {
accessTokenFactory: async () => {
@@ -517,7 +518,7 @@ export class Sync {
this.logger.error(
new Error("User encryption keys not generated. Please relogin.")
);
EV.publish(EVENTS.userSessionExpired);
this.db.eventManager.publish(EVENTS.userSessionExpired);
return false;
}
await this.processChunk(chunk, keys, options);

View File

@@ -19,10 +19,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import http from "../utils/http.js";
import constants from "../utils/constants.js";
import { EV, EVENTS } from "../common.js";
import { EVENTS } from "../common.js";
import { withTimeout, Mutex } from "async-mutex";
import { logger } from "../logger.js";
import { KVStorageAccessor } from "../interfaces.js";
import EventManager from "../utils/event-manager.js";
export type Token = {
access_token: string;
@@ -55,7 +56,10 @@ class TokenManager {
new Error("Timed out while refreshing access token.")
);
constructor(private readonly storage: KVStorageAccessor) {}
constructor(
private readonly storage: KVStorageAccessor,
private readonly eventManager: EventManager
) {}
async getToken(renew = true, forceRenew = false): Promise<Token | undefined> {
const token = await this.storage().read("token");
@@ -92,12 +96,16 @@ class TokenManager {
scopes: Scope[] = ["notesnook.sync", "IdentityServerApi"],
forceRenew = false
) {
return await getSafeToken(async () => {
const token = await this.getToken(true, forceRenew);
if (!token || !token.scope) return;
if (!scopes.some((s) => token.scope.includes(s))) return;
return token.access_token;
}, "Error getting access token:");
return await getSafeToken(
async () => {
const token = await this.getToken(true, forceRenew);
if (!token || !token.scope) return;
if (!scopes.some((s) => token.scope.includes(s))) return;
return token.access_token;
},
"Error getting access token:",
this.eventManager
);
}
async _refreshToken(forceRenew = false) {
@@ -112,7 +120,7 @@ class TokenManager {
const { refresh_token, scope } = token;
if (!refresh_token || !scope) {
EV.publish(EVENTS.userSessionExpired);
this.eventManager.publish(EVENTS.userSessionExpired);
this.logger.error(new Error("Token not found."));
return;
}
@@ -127,7 +135,7 @@ class TokenManager {
}
);
await this.saveToken(refreshTokenResponse);
EV.publish(EVENTS.tokenRefreshed);
this.eventManager.publish(EVENTS.tokenRefreshed);
});
}
@@ -163,7 +171,11 @@ class TokenManager {
}
export default TokenManager;
async function getSafeToken<T>(action: () => Promise<T>, errorMessage: string) {
async function getSafeToken<T>(
action: () => Promise<T>,
errorMessage: string,
eventManager: EventManager
) {
try {
return await action();
} catch (e) {
@@ -172,7 +184,7 @@ async function getSafeToken<T>(action: () => Promise<T>, errorMessage: string) {
e instanceof Error &&
(e.message === "invalid_grant" || e.message === "invalid_client")
) {
EV.publish(EVENTS.userSessionExpired);
eventManager.publish(EVENTS.userSessionExpired);
}
throw e;
}

View File

@@ -51,8 +51,8 @@ class UserManager {
private tokenManager: TokenManager;
private keyManager: KeyManager;
constructor(private readonly db: Database) {
this.tokenManager = new TokenManager(db.kv);
this.keyManager = new KeyManager(db);
this.tokenManager = new TokenManager(db.kv, db.eventManager);
EV.subscribe(EVENTS.userUnauthorized, async (url: string) => {
if (url.includes("/connect/token") || !(await HealthCheck.auth())) return;

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Cipher } from "@notesnook/crypto";
import Database from "./index.js";
import { EV, EVENTS } from "../common.js";
import { EVENTS } from "../common.js";
import { isCipher } from "../utils/crypto.js";
import { Note, NoteContent } from "../types.js";
import { logger } from "../logger.js";
@@ -48,7 +48,7 @@ export default class Vault {
}
private startEraser() {
EV.publish(EVENTS.vaultUnlocked);
this.db.eventManager.publish(EVENTS.vaultUnlocked);
clearTimeout(this.erasureTimeout);
this.erasureTimeout = setTimeout(() => {
this.lock();
@@ -80,7 +80,7 @@ export default class Vault {
async lock() {
this.password = undefined;
EV.publish(EVENTS.vaultLocked);
this.db.eventManager.publish(EVENTS.vaultLocked);
return true;
}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ICollection } from "./collection.js";
import { getId } from "../utils/id.js";
import { EV, EVENTS } from "../common.js";
import { EVENTS } from "../common.js";
import dataurl from "../utils/dataurl.js";
import dayjs from "dayjs";
import {
@@ -48,7 +48,7 @@ export class Attachments implements ICollection {
db.sanitizer
);
EV.subscribe(
db.eventManager.subscribe(
EVENTS.fileDownloaded,
async ({
success,
@@ -68,7 +68,7 @@ export class Attachments implements ICollection {
const src = await this.read(filename, getOutputType(attachment));
if (!src) return;
EV.publish(EVENTS.mediaAttachmentDownloaded, {
this.db.eventManager.publish(EVENTS.mediaAttachmentDownloaded, {
groupId,
hash: attachment.hash,
attachmentType: getAttachmentType(attachment),
@@ -77,7 +77,7 @@ export class Attachments implements ICollection {
}
);
EV.subscribe(
db.eventManager.subscribe(
EVENTS.fileUploaded,
async ({
success,

View File

@@ -28,11 +28,14 @@ export const SYNC_CHECK_IDS = {
export type SyncStatusEvent = keyof typeof SYNC_CHECK_IDS;
export async function checkSyncStatus(type: string) {
const results = await EV.publishWithResult<{ type: string; result: boolean }>(
EVENTS.syncCheckStatus,
type
);
export async function checkSyncStatus(
eventManager: EventManager,
type: string
) {
const results = await eventManager.publishWithResult<{
type: string;
result: boolean;
}>(EVENTS.syncCheckStatus, type);
if (typeof results === "boolean") return results;
else if (typeof results === "undefined") return true;
return results.some((r) => r.type === type && r.result === true);
@@ -44,23 +47,23 @@ export type SyncProgressEvent = {
};
export function sendSyncProgressEvent(
EV: EventManager,
eventManager: EventManager,
type: string,
current: number
) {
EV.publish(EVENTS.syncProgress, {
eventManager.publish(EVENTS.syncProgress, {
type,
current
} as SyncProgressEvent);
}
export function sendMigrationProgressEvent(
EV: EventManager,
eventManager: EventManager,
collection: string,
total: number,
current?: number
) {
EV.publish(EVENTS.migrationProgress, {
eventManager.publish(EVENTS.migrationProgress, {
collection,
total,
current: current === undefined ? total : current

View File

@@ -24,8 +24,9 @@ import {
IFileStorage
} from "../interfaces.js";
import { DataFormat, SerializedKey } from "@notesnook/crypto";
import { EV, EVENTS } from "../common.js";
import { EVENTS } from "../common.js";
import { logger } from "../logger.js";
import EventManager from "../utils/event-manager.js";
export type FileStorageAccessor = () => FileStorage;
export type DownloadableFile = {
@@ -48,7 +49,8 @@ export class FileStorage {
constructor(
private readonly fs: IFileStorage,
private readonly tokenManager: TokenManager
private readonly tokenManager: TokenManager,
private readonly eventManager: EventManager
) {}
async queueDownloads(
@@ -70,7 +72,7 @@ export class FileStorage {
for (const file of files as QueueItem[]) {
current++;
if (!group.has(file.filename)) {
EV.publish(EVENTS.fileDownloaded, {
this.eventManager.publish(EVENTS.fileDownloaded, {
success: false,
groupId,
filename: file.filename,
@@ -93,7 +95,7 @@ export class FileStorage {
const { filename, chunkSize } = file;
if (await this.exists(filename)) {
EV.publish(EVENTS.fileDownloaded, {
this.eventManager.publish(EVENTS.fileDownloaded, {
success: true,
groupId,
filename,
@@ -104,7 +106,7 @@ export class FileStorage {
continue;
}
EV.publish(EVENTS.fileDownload, {
this.eventManager.publish(EVENTS.fileDownload, {
total,
current,
groupId,
@@ -128,7 +130,7 @@ export class FileStorage {
this.downloads.set(filename, file);
const result = await file.operation;
if (eventData)
EV.publish(EVENTS.fileDownloaded, {
this.eventManager.publish(EVENTS.fileDownloaded, {
success: result,
total,
current,
@@ -180,7 +182,7 @@ export class FileStorage {
group.delete(filename);
});
EV.publish(EVENTS.fileUpload, {
this.eventManager.publish(EVENTS.fileUpload, {
total,
current,
groupId,
@@ -189,7 +191,7 @@ export class FileStorage {
this.uploads.set(filename, file);
const result = await file.operation;
EV.publish(EVENTS.fileUploaded, {
this.eventManager.publish(EVENTS.fileUploaded, {
error,
success: result,
total,
@@ -256,10 +258,16 @@ export class FileStorage {
if (queue.type === "download") {
this.groups.downloads.delete(groupId);
EV.publish(EVENTS.downloadCanceled, { groupId, canceled: true });
this.eventManager.publish(EVENTS.downloadCanceled, {
groupId,
canceled: true
});
} else if (queue.type === "upload") {
this.groups.uploads.delete(groupId);
EV.publish(EVENTS.uploadCanceled, { groupId, canceled: true });
this.eventManager.publish(EVENTS.uploadCanceled, {
groupId,
canceled: true
});
}
}
}