mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 19:19:34 +02:00
Compare commits
17 Commits
3.0.4-andr
...
fix/locked
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff92ca495d | ||
|
|
5b57614bfe | ||
|
|
ae7c538ad7 | ||
|
|
c6394c9e37 | ||
|
|
acf3740a68 | ||
|
|
8423543904 | ||
|
|
b90cb28637 | ||
|
|
780e1e9db0 | ||
|
|
ca6f8e5e4b | ||
|
|
4c4cda1c39 | ||
|
|
6fcbde772c | ||
|
|
eac1750b1c | ||
|
|
5eea179a58 | ||
|
|
fe1ea6dc27 | ||
|
|
cf1b774255 | ||
|
|
198fa33fc0 | ||
|
|
e722866d62 |
@@ -43,9 +43,8 @@ const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
|
||||
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
|
||||
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
@@ -117,9 +116,7 @@ export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
@@ -135,10 +132,8 @@ export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const decrypted = await decrypt(
|
||||
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
|
||||
appLockCipher
|
||||
);
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
@@ -213,8 +208,7 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
key: DB_KEY
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
@@ -238,8 +232,7 @@ export async function deriveCryptoKey(data) {
|
||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
key: await getDatabaseKey()
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
|
||||
@@ -34,11 +34,13 @@ import {
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
@@ -148,6 +150,12 @@ const AppLockedOverlay = () => {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -188,11 +196,7 @@ const AppLockedOverlay = () => {
|
||||
enabled(false);
|
||||
} else {
|
||||
SettingsService.appEnteredBackground();
|
||||
|
||||
if (
|
||||
SettingsService.get().privacyScreen ||
|
||||
SettingsService.getProperty("appLockEnabled")
|
||||
) {
|
||||
if (SettingsService.get().privacyScreen) {
|
||||
enabled(true);
|
||||
}
|
||||
}
|
||||
@@ -209,6 +213,7 @@ const AppLockedOverlay = () => {
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
|
||||
@@ -44,6 +44,7 @@ type DialogInfo = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
context: "global" | "local" | (string & {});
|
||||
secureTextEntry?: boolean;
|
||||
keyboardType?: string;
|
||||
};
|
||||
|
||||
export function presentDialog(data: Partial<DialogInfo>): void {
|
||||
|
||||
@@ -174,6 +174,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
onSubmit={onPressPositive}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
keyboardType={dialogInfo.keyboardType || "default"}
|
||||
placeholder={dialogInfo.inputPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
clearAppLockVerificationCipher,
|
||||
setAppLockVerificationCipher,
|
||||
@@ -44,6 +45,7 @@ import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Toast } from "../../toast";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
@@ -66,6 +68,7 @@ export const AppLockPassword = () => {
|
||||
confirmPassword?: string;
|
||||
}>({});
|
||||
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||
const [accountPass, setAccountPass] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const subs = [
|
||||
@@ -73,6 +76,7 @@ export const AppLockPassword = () => {
|
||||
eOpenAppLockPasswordDialog,
|
||||
(mode: "create" | "change" | "remove") => {
|
||||
setMode(mode);
|
||||
setAccountPass(false);
|
||||
setVisible(true);
|
||||
}
|
||||
),
|
||||
@@ -126,7 +130,9 @@ export const AppLockPassword = () => {
|
||||
mode === "change"
|
||||
? `Change app lock ${keyboardType}`
|
||||
: mode === "remove"
|
||||
? `Remove app lock ${keyboardType}`
|
||||
? `Enter ${
|
||||
accountPass ? "account password" : `app lock ${keyboardType}`
|
||||
} to remove ${keyboardType}`
|
||||
: `Set up a custom app lock ${keyboardType} to unlock the app`
|
||||
}
|
||||
icon="shield"
|
||||
@@ -169,33 +175,39 @@ export const AppLockPassword = () => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
defaultValue={values.current.password}
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
keyboardType={
|
||||
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
|
||||
}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
|
||||
returnKeyType={mode !== "remove" ? "next" : "done"}
|
||||
secureTextEntry={secureTextEntry}
|
||||
buttonLeft={
|
||||
<IconButton
|
||||
name={keyboardType === "password" ? "numeric" : "keyboard"}
|
||||
onPress={() => {
|
||||
setKeyboardType(
|
||||
keyboardType === "password" ? "pin" : "password"
|
||||
);
|
||||
setSecureTextEntry(false);
|
||||
setImmediate(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
accountPass ? null : (
|
||||
<IconButton
|
||||
name={keyboardType === "password" ? "numeric" : "keyboard"}
|
||||
onPress={() => {
|
||||
setKeyboardType(
|
||||
keyboardType === "password" ? "pin" : "password"
|
||||
);
|
||||
setSecureTextEntry(false);
|
||||
setImmediate(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
mode === "change"
|
||||
accountPass
|
||||
? "Account password"
|
||||
: mode === "change"
|
||||
? `New ${keyboardType}`
|
||||
: `${keyboardType === "pin" ? "Pin" : "Password"}`
|
||||
}
|
||||
@@ -222,6 +234,36 @@ export const AppLockPassword = () => {
|
||||
placeholder={`Confirm ${keyboardType}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{mode === "remove" ? (
|
||||
<>
|
||||
<Button
|
||||
icon={
|
||||
accountPass ? "checkbox-marked" : "checkbox-blank-outline"
|
||||
}
|
||||
onPress={() => {
|
||||
setSecureTextEntry(false);
|
||||
setAccountPass(!accountPass);
|
||||
setTimeout(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
iconSize={SIZE.lg}
|
||||
type="plain"
|
||||
iconColor={
|
||||
accountPass ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
title="Use account password instead"
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
height: 30
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<DialogButtons
|
||||
@@ -249,7 +291,8 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
const password = values.current.password;
|
||||
setAppLockVerificationCipher(password);
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
} else if (mode === "change") {
|
||||
if (
|
||||
@@ -277,7 +320,6 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.currentPassword
|
||||
);
|
||||
@@ -293,8 +335,10 @@ export const AppLockPassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = values.current.password;
|
||||
await clearAppLockVerificationCipher();
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
await setAppLockVerificationCipher(password);
|
||||
} else if (mode === "remove") {
|
||||
if (!values.current.password) {
|
||||
ToastManager.error(
|
||||
@@ -305,14 +349,18 @@ export const AppLockPassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.password
|
||||
);
|
||||
const isCurrentPasswordCorrect = accountPass
|
||||
? await db.user.verifyPassword(values.current.password)
|
||||
: await validateAppLockPassword(values.current.password);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
|
||||
accountPass
|
||||
? "Account password incorrect"
|
||||
: `${
|
||||
keyboardType === "pin" ? "Pin" : "Password"
|
||||
} incorrect`
|
||||
),
|
||||
undefined,
|
||||
"local"
|
||||
@@ -342,7 +390,9 @@ export const AppLockPassword = () => {
|
||||
|
||||
close();
|
||||
}}
|
||||
positiveTitle="Save"
|
||||
positiveTitle={
|
||||
mode === "remove" ? "Remove" : mode === "change" ? "Change" : "Save"
|
||||
}
|
||||
negativeTitle="Cancel"
|
||||
positiveType="transparent"
|
||||
loading={false}
|
||||
|
||||
@@ -21,10 +21,10 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Editor from "../../screens/editor";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useTrashStore } from "../../stores/use-trash-store";
|
||||
@@ -34,7 +34,7 @@ import DialogHeader from "../dialog/dialog-header";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
import { diff } from "diffblazer";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -121,12 +121,21 @@ export default function NotePreview({ session, content, note }) {
|
||||
<ReadonlyEditor
|
||||
editorId="historyPreview"
|
||||
onLoad={async (loadContent) => {
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
loadContent({
|
||||
data: content.data,
|
||||
id: _note.id
|
||||
});
|
||||
try {
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
const currentContent = await db.content.get(_note.contentId);
|
||||
loadContent({
|
||||
data: diff(currentContent.data, content.data),
|
||||
id: _note.id
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
ToastManager.error(
|
||||
e,
|
||||
"Failed to load history preview",
|
||||
"local"
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -22,20 +22,14 @@ import { Platform } from "react-native";
|
||||
import { Subscription } from "react-native-iap";
|
||||
import PremiumService from "../services/premium";
|
||||
import { db } from "../common/database";
|
||||
import { Product } from "@notesnook/core/dist/api/pricing";
|
||||
|
||||
type PurchaseInfo = {
|
||||
country: string;
|
||||
countryCode: string;
|
||||
sku: string;
|
||||
discount: number;
|
||||
};
|
||||
|
||||
const skuInfos: { [name: string]: PurchaseInfo | undefined } = {};
|
||||
const skuInfos: { [name: string]: Product | undefined } = {};
|
||||
|
||||
export const usePricing = (period: "monthly" | "yearly") => {
|
||||
const [current, setCurrent] = useState<{
|
||||
period: string;
|
||||
info?: PurchaseInfo;
|
||||
info?: Product;
|
||||
product?: Subscription;
|
||||
}>();
|
||||
|
||||
@@ -54,6 +48,7 @@ export const usePricing = (period: "monthly" | "yearly") => {
|
||||
period
|
||||
));
|
||||
skuInfos[period] = skuInfo;
|
||||
|
||||
const products = (await PremiumService.getProducts()) as Subscription[];
|
||||
let product = products.find((p) => p.productId === skuInfo?.sku);
|
||||
if (!product)
|
||||
|
||||
@@ -75,14 +75,16 @@ export function ReadonlyEditor(props: {
|
||||
if (editorMessage.type === EventTypes.readonlyEditorLoaded) {
|
||||
console.log("Readonly editor loaded.");
|
||||
props.onLoad?.((content: { data: string; id: string }) => {
|
||||
noteId.current = content.id;
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: "native:html",
|
||||
value: content.data
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
noteId.current = content.id;
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: "native:html",
|
||||
value: content.data
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
}, 300);
|
||||
});
|
||||
} else if (editorMessage.type === EventTypes.getAttachmentData) {
|
||||
const attachment = (editorMessage.value as any).attachment as Attachment;
|
||||
|
||||
@@ -351,9 +351,18 @@ export const useEditor = (
|
||||
if (!unlocked)
|
||||
throw new Error("Could not save note, vault is locked");
|
||||
}
|
||||
noteData.contentId = note?.contentId;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await db.vault?.save(noteData as any);
|
||||
if (typeof noteData.title === "string") {
|
||||
await db.notes.add({
|
||||
title: noteData.title,
|
||||
id: noteData.id
|
||||
});
|
||||
}
|
||||
|
||||
noteData.contentId = note?.contentId;
|
||||
if (data) {
|
||||
await db.vault?.save(noteData as any);
|
||||
}
|
||||
clearTimeout(saveTimer);
|
||||
}
|
||||
|
||||
@@ -696,14 +705,13 @@ export const useEditor = (
|
||||
);
|
||||
}
|
||||
|
||||
console.log("readonly state changed...", note.readonly);
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
readonly: note.readonly
|
||||
});
|
||||
}
|
||||
|
||||
if (data.type === "tiptap" && note && !isLocal) {
|
||||
if (lastContentChangeTime.current[noteId] >= data.dateEdited) {
|
||||
if (lastContentChangeTime.current[noteId] >= data.dateModified) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -728,7 +736,7 @@ export const useEditor = (
|
||||
}
|
||||
} else {
|
||||
const _nextContent = data.data;
|
||||
if (_nextContent === currentContents.current?.data) {
|
||||
if (_nextContent === currentContents.current[note.id]?.data) {
|
||||
return;
|
||||
}
|
||||
lastContentChangeTime.current[note.id] = note.dateEdited;
|
||||
|
||||
@@ -90,13 +90,26 @@ export async function verifyUserWithApplock() {
|
||||
positiveText: "Verify",
|
||||
secureTextEntry: true,
|
||||
negativeText: "Cancel",
|
||||
keyboardType: keyboardType,
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
const verified = await validateAppLockPassword(value);
|
||||
if (!verified) {
|
||||
ToastManager.show({
|
||||
heading: `Invalid ${
|
||||
keyboardType === "numeric" ? "pin" : "password"
|
||||
}`,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
resolve(verified);
|
||||
} catch (e) {
|
||||
resolve(false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -871,6 +871,9 @@ export const settingsGroups: SettingSection[] = [
|
||||
type: "switch",
|
||||
property: "appLockEnabled",
|
||||
onChange: () => {
|
||||
SettingsService.set({
|
||||
privacyScreen: true
|
||||
});
|
||||
SettingsService.setPrivacyScreen(SettingsService.get());
|
||||
},
|
||||
onVerify: async () => {
|
||||
@@ -973,7 +976,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
SettingsService.getProperty("applockKeyboardType") === "numeric"
|
||||
? "pin"
|
||||
: "password"
|
||||
}, app lock will fallback to using account password to unlock the app`,
|
||||
}, app lock will be disabled if no other security method is enabled.`,
|
||||
hidden: () => {
|
||||
return !SettingsService.getProperty("appLockHasPasswordSecurity");
|
||||
},
|
||||
|
||||
@@ -76,6 +76,17 @@ function migrateAppLock() {
|
||||
DatabaseLogger.debug("App lock Migrated");
|
||||
}
|
||||
|
||||
function migrateSettings(settings: SettingStore["settings"]) {
|
||||
const version = settings.settingsVersion;
|
||||
if (!version) {
|
||||
settings.settingsVersion = 1;
|
||||
settings.privacyScreen = settings.appLockEnabled
|
||||
? true
|
||||
: settings.privacyScreen;
|
||||
MMKV.setString("appSettings", JSON.stringify(settings));
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
scale.fontScale = 1;
|
||||
const settingsJson = MMKV.getString("appSettings");
|
||||
@@ -83,14 +94,17 @@ function init() {
|
||||
if (!settingsJson) {
|
||||
MMKV.setString("appSettings", JSON.stringify(settings));
|
||||
} else {
|
||||
const settingsParsed = JSON.parse(settingsJson);
|
||||
migrateSettings(settingsParsed);
|
||||
settings = {
|
||||
...settings,
|
||||
...JSON.parse(settingsJson)
|
||||
...settingsParsed
|
||||
};
|
||||
}
|
||||
if (settings.fontScale) {
|
||||
scale.fontScale = settings.fontScale;
|
||||
}
|
||||
|
||||
setTimeout(() => setPrivacyScreen(settings), 1);
|
||||
updateSize();
|
||||
useSettingStore.getState().setSettings({ ...settings });
|
||||
@@ -98,7 +112,7 @@ function init() {
|
||||
}
|
||||
|
||||
function setPrivacyScreen(settings: SettingStore["settings"]) {
|
||||
if (settings.privacyScreen || settings.appLockEnabled) {
|
||||
if (settings.privacyScreen) {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(true);
|
||||
} else {
|
||||
|
||||
@@ -81,6 +81,7 @@ export type Settings = {
|
||||
biometricsAuthEnabled?: boolean;
|
||||
backgroundSync?: boolean;
|
||||
applockKeyboardType: "numeric" | "default";
|
||||
settingsVersion?: number;
|
||||
};
|
||||
|
||||
type DimensionsType = {
|
||||
@@ -169,7 +170,8 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
markdownShortcuts: true,
|
||||
biometricsAuthEnabled: false,
|
||||
appLockHasPasswordSecurity: false,
|
||||
backgroundSync: true
|
||||
backgroundSync: true,
|
||||
settingsVersion: 0
|
||||
};
|
||||
|
||||
export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
|
||||
@@ -331,12 +331,6 @@ async function resolveConflict({
|
||||
toKeepDateEdited: number;
|
||||
dateResolved?: number;
|
||||
}) {
|
||||
await db.notes.add({
|
||||
id: note.id,
|
||||
dateEdited: toKeepDateEdited,
|
||||
conflicted: false
|
||||
});
|
||||
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
data: toKeep,
|
||||
@@ -345,12 +339,17 @@ async function resolveConflict({
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
|
||||
await db.notes.add({
|
||||
id: note.id,
|
||||
dateEdited: toKeepDateEdited,
|
||||
conflicted: false
|
||||
});
|
||||
|
||||
if (toCopy) {
|
||||
await createCopy(note, toCopy);
|
||||
}
|
||||
|
||||
await notesStore.refresh();
|
||||
useEditorStore.getState().openSession(note.id, { force: true });
|
||||
}
|
||||
|
||||
async function createCopy(note: Note, content: ContentItem) {
|
||||
|
||||
@@ -271,7 +271,7 @@ function EditorView({
|
||||
const isContent =
|
||||
item.type === "tiptap" && item.noteId === session.note.id;
|
||||
const isNote = item.type === "note" && item.id === session.note.id;
|
||||
if (isContent && lastChangedTime.current < item.dateEdited) {
|
||||
if (isContent && lastChangedTime.current < item.dateModified) {
|
||||
if (!item.locked) return editor.updateContent(item.data);
|
||||
|
||||
const result = await db.vault
|
||||
|
||||
@@ -162,9 +162,10 @@ function getErrorHelp(props: FallbackProps) {
|
||||
if (
|
||||
errorText.includes("file is not a database") ||
|
||||
errorText.includes("unsupported file format") ||
|
||||
errorText.includes("database disk image is malformed") ||
|
||||
errorText.includes("null function or function signature mismatch") ||
|
||||
errorText.includes("malformed database schema")
|
||||
errorText.includes("malformed database schema") ||
|
||||
/table ".+?" already exists/.test(errorText) ||
|
||||
errorText.includes("corrupted migrations:")
|
||||
) {
|
||||
return {
|
||||
explanation: `This error usually means the database file is either corrupt or it could not be decrypted.`,
|
||||
@@ -195,6 +196,17 @@ function getErrorHelp(props: FallbackProps) {
|
||||
resetErrorBoundary();
|
||||
}
|
||||
};
|
||||
} else if (errorText.includes("database disk image is malformed")) {
|
||||
return {
|
||||
explanation: `This error usually means the search index is corrupted.`,
|
||||
action:
|
||||
"This error can be fixed by rebuilding the search index. This action won't result in any kind of data loss.",
|
||||
fix: async () => {
|
||||
const { db } = await import("../../common/db");
|
||||
await db.lookup.rebuild();
|
||||
resetErrorBoundary();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -233,6 +233,15 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
if (noteId && session.id !== noteId && session.note.id !== noteId)
|
||||
continue;
|
||||
if (isDeleted(item) || isTrashItem(item)) clearIds.push(session.id);
|
||||
// if a note becomes conflicted, reopen the session
|
||||
else if (
|
||||
session.type !== "conflicted" &&
|
||||
item.type === "tiptap" &&
|
||||
item.conflicted
|
||||
)
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
// if a note is locked, reopen the session
|
||||
else if (
|
||||
session.type === "default" &&
|
||||
@@ -257,9 +266,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
}
|
||||
// if a deleted note is restored, reopen the session
|
||||
else if (session.type === "deleted" && item.type === "note") {
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
openSession(session.note.id, { force: true, silent: true });
|
||||
}
|
||||
// if a readonly note is made editable, reopen the session
|
||||
else if (
|
||||
@@ -267,9 +274,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
item.type === "note" &&
|
||||
!item.readonly
|
||||
)
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
openSession(session.note.id, { force: true, silent: true });
|
||||
// update the note in all sessions
|
||||
else if (item.type === "note") {
|
||||
updateSession(
|
||||
@@ -344,6 +349,10 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
session.type !== "new" && session.note.contentId;
|
||||
if (!contentId || !event.ids.includes(contentId)) continue;
|
||||
if (
|
||||
// if note's conflict is resolved
|
||||
(session.type === "conflicted" && !event.item.conflicted) ||
|
||||
// if note becomes conflicted
|
||||
(session.type !== "conflicted" && event.item.conflicted) ||
|
||||
// if note is locked
|
||||
(session.type === "default" &&
|
||||
!session.locked &&
|
||||
@@ -548,12 +557,14 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const noteId = typeof noteOrId === "string" ? noteOrId : noteOrId.id;
|
||||
const session = getSession(noteId);
|
||||
|
||||
if (session && !options.force && !session.needsHydration) {
|
||||
return this.activateSession(noteId, options.activeBlockId);
|
||||
}
|
||||
if (session && !options.force) {
|
||||
if (!session.needsHydration) {
|
||||
return this.activateSession(noteId, options.activeBlockId);
|
||||
}
|
||||
|
||||
if (session && (session.type === "diff" || session.type === "conflicted")) {
|
||||
return openDiffSession(session.note.id, session.id);
|
||||
if (session.type === "diff" || session.type === "conflicted") {
|
||||
return openDiffSession(session.note.id, session.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (session && session.id) await db.fs().cancel(session.id);
|
||||
@@ -566,19 +577,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const isPreview = session ? session.preview : !options?.newSession;
|
||||
const isLocked = await db.vaults.itemExists(note);
|
||||
|
||||
if (isLocked && note.type !== "trash") {
|
||||
this.addSession(
|
||||
{
|
||||
type: "locked",
|
||||
id: note.id,
|
||||
pinned: session?.pinned,
|
||||
note,
|
||||
preview: isPreview,
|
||||
activeBlockId: options.activeBlockId
|
||||
},
|
||||
!options.silent
|
||||
);
|
||||
} else if (note.conflicted) {
|
||||
if (note.conflicted) {
|
||||
const content = note.contentId
|
||||
? await db.content.get(note.contentId)
|
||||
: undefined;
|
||||
@@ -612,6 +611,18 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
},
|
||||
!options.silent
|
||||
);
|
||||
} else if (isLocked && note.type !== "trash") {
|
||||
this.addSession(
|
||||
{
|
||||
type: "locked",
|
||||
id: note.id,
|
||||
pinned: session?.pinned,
|
||||
note,
|
||||
preview: isPreview,
|
||||
activeBlockId: options.activeBlockId
|
||||
},
|
||||
!options.silent
|
||||
);
|
||||
} else {
|
||||
const content = note.contentId
|
||||
? await db.content.get(note.contentId)
|
||||
|
||||
@@ -28,7 +28,7 @@ import { login } from "./utils";
|
||||
import { SqliteDialect } from "kysely";
|
||||
import BetterSQLite3 from "better-sqlite3-multiple-ciphers";
|
||||
|
||||
const TEST_TIMEOUT = 30 * 1000;
|
||||
const TEST_TIMEOUT = 60 * 1000;
|
||||
|
||||
test(
|
||||
"case 1: device A & B should only download the changes from device C (no uploading)",
|
||||
@@ -156,6 +156,111 @@ test(
|
||||
TEST_TIMEOUT * 10
|
||||
);
|
||||
|
||||
test(
|
||||
"issue: syncing should not affect the items' dateModified",
|
||||
async (t) => {
|
||||
const [deviceA] = await Promise.all([initializeDevice("deviceA")]);
|
||||
|
||||
t.onTestFinished(async (r) => {
|
||||
console.log(`${t.task.name} log out`);
|
||||
await cleanup(deviceA);
|
||||
});
|
||||
|
||||
const noteId = await deviceA.notes.add({
|
||||
title: "Test note from device A",
|
||||
content: { data: "<p>Hello</p>", type: "tiptap" }
|
||||
});
|
||||
const noteDateBefore = (await deviceA.notes.note(noteId)).dateModified;
|
||||
const contentDateBefore = (await deviceA.content.findByNoteId(noteId))
|
||||
.dateModified;
|
||||
await deviceA.sync({ type: "full" });
|
||||
const noteDateAfter = (await deviceA.notes.note(noteId)).dateModified;
|
||||
const contentDateAfter = (await deviceA.content.findByNoteId(noteId))
|
||||
.dateModified;
|
||||
expect(noteDateBefore).toBe(noteDateAfter);
|
||||
expect(contentDateBefore).toBe(contentDateAfter);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"case 4: local content changed after remote content should create a conflict",
|
||||
async (t) => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB")
|
||||
]);
|
||||
|
||||
t.onTestFinished(async (r) => {
|
||||
console.log(`${t.task.name} log out`);
|
||||
await cleanup(deviceA, deviceB);
|
||||
});
|
||||
|
||||
const noteId = await deviceA.notes.add({
|
||||
title: "Test note from device A",
|
||||
content: { data: "<p>Hello</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
await deviceB.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device B)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
await deviceA.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device A)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
|
||||
expect(await deviceA.notes.conflicted.count()).toBeGreaterThan(0);
|
||||
},
|
||||
TEST_TIMEOUT * 10
|
||||
);
|
||||
|
||||
test(
|
||||
"case 5: remote content changed after local content should create a conflict",
|
||||
async (t) => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB")
|
||||
]);
|
||||
|
||||
t.onTestFinished(async (r) => {
|
||||
console.log(`${t.task.name} log out`);
|
||||
await cleanup(deviceA, deviceB);
|
||||
});
|
||||
|
||||
const noteId = await deviceA.notes.add({
|
||||
title: "Test note from device A",
|
||||
content: { data: "<p>Hello</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
await deviceA.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device B)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
await deviceB.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device A)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
expect(await deviceB.notes.conflicted.count()).toBeGreaterThan(0);
|
||||
},
|
||||
TEST_TIMEOUT * 10
|
||||
);
|
||||
|
||||
// test(
|
||||
// "case 4: Device A's sync is interrupted halfway and Device B makes some changes afterwards and syncs.",
|
||||
// async () => {
|
||||
@@ -378,6 +483,8 @@ test(
|
||||
* @returns {Promise<Database>}
|
||||
*/
|
||||
async function initializeDevice(id, capabilities = []) {
|
||||
// initialize(new NodeStorageInterface(), false);
|
||||
|
||||
console.time(`Init ${id}`);
|
||||
EV.subscribe(EVENTS.userCheckStatus, async (type) => {
|
||||
return {
|
||||
|
||||
@@ -179,7 +179,7 @@ class Database {
|
||||
subscriptions = new Subscriptions(this.tokenManager);
|
||||
offers = new Offers();
|
||||
debug = new Debug();
|
||||
pricing = new Pricing();
|
||||
pricing = Pricing;
|
||||
|
||||
user = new UserManager(this);
|
||||
syncer = new Sync(this);
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import http from "../utils/http";
|
||||
|
||||
type Product = {
|
||||
export type Product = {
|
||||
country: string;
|
||||
countryCode: string;
|
||||
sku?: string;
|
||||
|
||||
@@ -57,37 +57,34 @@ class Collector {
|
||||
const collection = this.db[collectionKey].collection;
|
||||
let pushTimestamp = Date.now();
|
||||
for await (const chunk of collection.unsynced(chunkSize, isForceSync)) {
|
||||
const items = await this.prepareChunk(chunk, key);
|
||||
const { ids, items: syncableItems } = filterSyncableItems(chunk);
|
||||
if (!ids.length) continue;
|
||||
const ciphers = await this.db
|
||||
.storage()
|
||||
.encryptMulti(key, syncableItems);
|
||||
const items = toPushItem(ids, ciphers);
|
||||
if (!items) continue;
|
||||
yield { items, type: itemType };
|
||||
|
||||
await collection.update(
|
||||
chunk.map((i) => i.id),
|
||||
{ synced: true },
|
||||
{
|
||||
sendEvent: false,
|
||||
// EDGE CASE:
|
||||
// Sometimes an item can get updated while it's being pushed.
|
||||
// The result is that its `synced` property becomes true even
|
||||
// though it's modification wasn't yet synced.
|
||||
// In order to prevent that, we only set the `synced` property
|
||||
// to true for items that haven't been modified since we last ran
|
||||
// the push. Everything else will be collected again in the next
|
||||
// push.
|
||||
condition: (eb) => eb("dateModified", "<=", pushTimestamp)
|
||||
}
|
||||
);
|
||||
await this.db
|
||||
.sql()
|
||||
.updateTable(collection.type)
|
||||
.where("id", "in", ids)
|
||||
// EDGE CASE:
|
||||
// Sometimes an item can get updated while it's being pushed.
|
||||
// The result is that its `synced` property becomes true even
|
||||
// though it's modification wasn't yet synced.
|
||||
// In order to prevent that, we only set the `synced` property
|
||||
// to true for items that haven't been modified since we last ran
|
||||
// the push. Everything else will be collected again in the next
|
||||
// push.
|
||||
.where("dateModified", "<=", pushTimestamp)
|
||||
.set({ synced: true })
|
||||
.execute();
|
||||
pushTimestamp = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async prepareChunk(chunk: MaybeDeletedItem<Item>[], key: SerializedKey) {
|
||||
const { ids, items } = filterSyncableItems(chunk);
|
||||
if (!ids.length) return;
|
||||
const ciphers = await this.db.storage().encryptMulti(key, items);
|
||||
return toPushItem(ids, ciphers);
|
||||
}
|
||||
}
|
||||
export default Collector;
|
||||
|
||||
@@ -113,9 +110,6 @@ function filterSyncableItems(items: MaybeDeletedItem<Item>[]): {
|
||||
const ids = [];
|
||||
const syncableItems = [];
|
||||
for (const item of items) {
|
||||
// do not sync conflicted note or content
|
||||
if ("conflicted" in item && item.conflicted) continue;
|
||||
|
||||
delete item.synced;
|
||||
|
||||
ids.push(item.id);
|
||||
|
||||
@@ -242,6 +242,20 @@ class Sync {
|
||||
|
||||
this.connection?.off("SendItems");
|
||||
this.connection?.off("SendVaultKey");
|
||||
|
||||
await this.db
|
||||
.sql()
|
||||
.updateTable("notes")
|
||||
.where("id", "in", (eb) =>
|
||||
eb
|
||||
.selectFrom("content")
|
||||
.select("noteId as id")
|
||||
.where("conflicted", "is not", null)
|
||||
.where("conflicted", "is not", false)
|
||||
.$castTo<string | null>()
|
||||
)
|
||||
.set({ conflicted: true })
|
||||
.execute();
|
||||
}
|
||||
|
||||
async send(deviceId: string, isForceSync?: boolean) {
|
||||
@@ -334,10 +348,8 @@ class Sync {
|
||||
const localItems = await collection.records(chunk.items.map((i) => i.id));
|
||||
let items: (MaybeDeletedItem<Item> | undefined)[] = [];
|
||||
if (itemType === "content") {
|
||||
items = await Promise.all(
|
||||
deserialized.map((item) =>
|
||||
this.merger.mergeContent(item, localItems[item.id])
|
||||
)
|
||||
items = deserialized.map((item) =>
|
||||
this.merger.mergeContent(item, localItems[item.id])
|
||||
);
|
||||
} else {
|
||||
items =
|
||||
|
||||
@@ -46,7 +46,7 @@ class Merger {
|
||||
}
|
||||
}
|
||||
|
||||
async mergeContent(
|
||||
mergeContent(
|
||||
remoteItem: MaybeDeletedItem<Item>,
|
||||
localItem: MaybeDeletedItem<Item> | undefined
|
||||
) {
|
||||
@@ -75,10 +75,7 @@ class Merger {
|
||||
else if (!conflicted) return;
|
||||
|
||||
// otherwise we trigger the conflicts
|
||||
await this.db.notes.add({
|
||||
id: localItem.noteId,
|
||||
conflicted: true
|
||||
});
|
||||
this.logger.info("conflict marked", { id: localItem.noteId });
|
||||
localItem.conflicted = remoteItem;
|
||||
return localItem;
|
||||
}
|
||||
@@ -124,9 +121,8 @@ export function isContentConflicted(
|
||||
remoteItem.dateModified &&
|
||||
localItem.dateResolved === remoteItem.dateModified;
|
||||
const isEdited =
|
||||
// the local item is edited if it was changed/edited after the remote
|
||||
// note and it also wasn't synced yet.
|
||||
localItem.dateEdited > remoteItem.dateEdited && !localItem.synced;
|
||||
// the local item is edited if it wasn't synced yet.
|
||||
!localItem.synced;
|
||||
if (isEdited && !isResolved) {
|
||||
// If time difference between local item's edits & remote item's edits
|
||||
// is less than threshold, we shouldn't trigger a merge conflict; instead
|
||||
|
||||
@@ -175,10 +175,7 @@ export class Content implements ICollection {
|
||||
const content = await this.collection.get(id);
|
||||
if (!content || isDeleted(content)) return;
|
||||
if (!content.locked && this.preProcess(content)) {
|
||||
await this.db.content.add({
|
||||
...content,
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
await this.collection.update([content.id], content, { modify: false });
|
||||
}
|
||||
return content;
|
||||
}
|
||||
@@ -246,10 +243,7 @@ export class Content implements ICollection {
|
||||
.executeTakeFirst()) as ContentItem;
|
||||
if (!content || isDeleted(content)) return;
|
||||
if (!content.locked && this.preProcess(content)) {
|
||||
await this.db.content.add({
|
||||
...content,
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
await this.collection.update([content.id], content, { modify: false });
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ type AsyncOrSyncResult<Async extends boolean, Response> = Async extends true
|
||||
: Response;
|
||||
|
||||
export interface DatabaseCollection<T, IsAsync extends boolean> {
|
||||
type: keyof DatabaseSchema;
|
||||
clear(): Promise<void>;
|
||||
init(): Promise<void>;
|
||||
upsert(item: T): Promise<void>;
|
||||
|
||||
@@ -38,7 +38,7 @@ export class SQLCachedCollection<
|
||||
startTransaction: (
|
||||
executor: (tr: Kysely<DatabaseSchema>) => Promise<void>
|
||||
) => Promise<void>,
|
||||
type: TCollectionType,
|
||||
public type: TCollectionType,
|
||||
eventManager: EventManager,
|
||||
sanitizer: Sanitizer
|
||||
) {
|
||||
|
||||
@@ -75,7 +75,7 @@ export class SQLCollection<
|
||||
_startTransaction: (
|
||||
executor: (tr: Kysely<DatabaseSchema>) => Promise<void>
|
||||
) => Promise<void>,
|
||||
private readonly type: TCollectionType,
|
||||
public readonly type: TCollectionType,
|
||||
private readonly eventManager: EventManager,
|
||||
private readonly sanitizer: Sanitizer
|
||||
) {}
|
||||
@@ -227,14 +227,16 @@ export class SQLCollection<
|
||||
ids: string[],
|
||||
partial: Partial<SQLiteItem<T>>,
|
||||
options: {
|
||||
sendEvent: boolean;
|
||||
sendEvent?: boolean;
|
||||
modify?: boolean;
|
||||
condition?: ExpressionOrFactory<
|
||||
DatabaseSchema,
|
||||
keyof DatabaseSchema,
|
||||
SqlBool
|
||||
>;
|
||||
} = { sendEvent: true }
|
||||
} = {}
|
||||
) {
|
||||
const { sendEvent = true, modify = true, condition } = options;
|
||||
if (!this.sanitizer.sanitize(this.type, partial)) return;
|
||||
|
||||
await this.db()
|
||||
@@ -244,16 +246,16 @@ export class SQLCollection<
|
||||
await tx
|
||||
.updateTable<keyof DatabaseSchema>(this.type)
|
||||
.where("id", "in", chunk)
|
||||
.$if(!!options.condition, (eb) => eb.where(options.condition!))
|
||||
.$if(!!condition, (eb) => eb.where(condition!))
|
||||
.set({
|
||||
...partial,
|
||||
dateModified: Date.now(),
|
||||
dateModified: modify ? Date.now() : undefined,
|
||||
synced: partial.synced || false
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
if (options.sendEvent) {
|
||||
if (sendEvent) {
|
||||
this.eventManager.publish(EVENTS.databaseUpdated, <UpdateEvent>{
|
||||
type: "update",
|
||||
collection: this.type,
|
||||
@@ -284,6 +286,12 @@ export class SQLCollection<
|
||||
.selectFrom<keyof DatabaseSchema>(this.type)
|
||||
.select((a) => a.fn.count<number>("id").as("count"))
|
||||
.where(isFalse("synced"))
|
||||
.$if(this.type === "content", (eb) =>
|
||||
eb.where("conflicted", "is", null)
|
||||
)
|
||||
.$if(this.type === "notes", (eb) =>
|
||||
eb.where("conflicted", "is not", true)
|
||||
)
|
||||
.$if(this.type === "attachments", (eb) =>
|
||||
eb.where((eb) =>
|
||||
eb.or([eb("dateUploaded", ">", 0), eb("deleted", "==", true)])
|
||||
@@ -304,6 +312,12 @@ export class SQLCollection<
|
||||
.selectAll()
|
||||
.$if(lastRowId != null, (qb) => qb.where("id", ">", lastRowId!))
|
||||
.$if(!forceSync, (eb) => eb.where(isFalse("synced")))
|
||||
.$if(this.type === "content", (eb) =>
|
||||
eb.where("conflicted", "is", null)
|
||||
)
|
||||
.$if(this.type === "notes", (eb) =>
|
||||
eb.where("conflicted", "is not", true)
|
||||
)
|
||||
.$if(this.type === "attachments", (eb) =>
|
||||
eb.where((eb) =>
|
||||
eb.or([eb("dateUploaded", ">", 0), eb("deleted", "==", true)])
|
||||
|
||||
Reference in New Issue
Block a user