Compare commits

..

4 Commits

Author SHA1 Message Date
Ammar Ahmed
dda177a944 mobile: fix encryption on ios 2024-12-21 14:15:29 +05:00
Ammar Ahmed
cc75f6588a core: fix fallback key encryption 2024-12-21 14:14:24 +05:00
Abdullah Atta
ee8701eff8 core: add support for fallback hashing
on ios some users using passwords with multi byte characters
generates the wrong hash. This commit adds support for fallback
to allow those users to continue to login once the fix has been applied.
2024-12-21 14:14:24 +05:00
Ammar Ahmed
fe3c5484c3 mobile: fix encryption on ios 2024-12-21 14:14:24 +05:00
425 changed files with 62840 additions and 33881 deletions

View File

@@ -14,8 +14,6 @@ runs:
apps/mobile/package-lock.json
apps/desktop/package-lock.json
apps/web/package-lock.json
apps/monograph/package-lock.json
apps/theme-builder/package-lock.json
extensions/web-clipper/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
@@ -26,4 +24,3 @@ runs:
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
packages/intl/package-lock.json

View File

@@ -4,7 +4,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache

View File

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

View File

@@ -37,7 +37,7 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -49,13 +49,6 @@ jobs:
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Setup environment
run: |
@@ -64,10 +57,6 @@ jobs:
- name: Generate desktop build
run: npx nx build:desktop @notesnook/web
- name: Build desktop bundle
working-directory: ./apps/desktop
run: npm run bundle
- name: Archive build artifact
uses: actions/upload-artifact@v4
with:
@@ -82,8 +71,7 @@ jobs:
- name: Package desktop build
run: |
cp -r ./apps/web/build ./apps/desktop/
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/desktop/build/
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/web/build/
echo "Build folder archived to ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip"
- name: Upload desktop build
@@ -99,7 +87,7 @@ jobs:
- name: Generate flatpak sources
if: inputs.publish-github && inputs.build-linux
run: |
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
sudo apt-get install -y flatpak flatpak-builder pipx
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
cd /tmp/flatpak-builder-tools/node
pipx install .

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -49,7 +49,7 @@ jobs:
name: 🧪 Test (${{ matrix.shard }}/${{ strategy.job-total }})
strategy:
matrix:
shard: [1, 2, 3, 4, 5]
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
steps:
- name: Checkout

1
.gitignore vendored
View File

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.28",
"version": "3.0.22",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
@@ -25,7 +25,7 @@
"repository": "https://github.com/streetwriters/notesnook",
"license": "GPL-3.0-or-later",
"dependencies": {
"@lingui/core": "5.1.2",
"@lingui/core": "4.11.4",
"@notesnook/intl": "file:../../packages/intl",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
@@ -33,10 +33,10 @@
"electron-trpc": "0.6.1",
"electron-updater": "^6.3.4",
"icojs": "^0.19.4",
"sqlite-better-trigram": "0.0.2",
"sqlite-better-trigram": "^0.0.2",
"typed-emitter": "^2.1.0",
"yargs": "^17.7.2",
"zod": "3.23.8"
"zod": "^3.23.8"
},
"devDependencies": {
"@streetwriters/kysely": "^0.27.4",
@@ -45,15 +45,15 @@
"chokidar": "^4.0.1",
"electron": "^31.7.4",
"electron-builder": "^25.1.8",
"esbuild": "0.21.5",
"vitest": "2.1.8",
"esbuild": "^0.24.0",
"node-abi": "^3.68.0",
"node-gyp-build": "^4.8.2",
"playwright": "^1.48.2",
"prebuildify": "^6.0.1",
"slugify": "1.6.6",
"slugify": "^1.6.6",
"tree-kill": "^1.2.2",
"undici": "^6.19.8"
"undici": "^6.19.8",
"vitest": "^2.1.5"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"

View File

@@ -36,7 +36,7 @@ await fs.rm("./build/", { force: true, recursive: true });
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
await exec(
"npx nx build:desktop @notesnook/web",
"yarn nx build:desktop @notesnook/web",
path.join(__dirname, "..", "..", "..")
);
}
@@ -58,13 +58,11 @@ if (args.variant === "mas") {
await exec(`yarn run build`);
if (args.run) {
await exec(`yarn electron-builder --dir --${process.arch}`);
await exec(`yarn electron-builder --dir --x64`);
if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") {
if (process.arch === "arm64")
await exec(`./output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook`);
else await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
} else {
await exec(`./output/linux-unpacked/Notesnook`);
}

View File

@@ -70,8 +70,8 @@ async function onChange(first) {
if (first) {
await spawnAndWaitUntil(
["npm", "run", "start:desktop"],
path.join(__dirname, "..", "..", "web"),
["yarn", "nx", "start:desktop", "@notesnook/web"],
path.join(__dirname, "..", "..", ".."),
(data) => data.includes("Network: use --host to expose")
);
}

View File

@@ -32,7 +32,6 @@ import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import { useAppEvents } from "./hooks/use-app-events";
import { ApplicationHolder } from "./navigation";
import { NotePreviewConfigure } from "./screens/note-preview-configure";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
@@ -48,7 +47,7 @@ if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
const App = (props: { configureMode: "note-preview" }) => {
const App = () => {
useAppEvents();
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
@@ -62,7 +61,6 @@ const App = (props: { configureMode: "note-preview" }) => {
TipManager.init();
}, 100);
}, []);
return (
<View
style={{
@@ -92,11 +90,7 @@ const App = (props: { configureMode: "note-preview" }) => {
width: "100%"
}}
>
{props.configureMode === "note-preview" ? (
<NotePreviewConfigure />
) : (
<ApplicationHolder />
)}
<ApplicationHolder />
</GestureHandlerRootView>
<AppLockedOverlay />
</View>
@@ -109,8 +103,8 @@ let currTheme =
: SettingsService.getProperty("lighTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: (props: any) => JSX.Element) => {
return function AppWithThemeProvider(props: any) {
export const withTheme = (Element: () => JSX.Element) => {
return function AppWithThemeProvider() {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,
state.darkTheme,
@@ -129,14 +123,13 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
})
.then((theme) => {
if (theme) {
console.log(theme.version, "theme updated");
theme.colorScheme === "dark"
? useThemeStore.getState().setDarkTheme(theme)
: useThemeStore.getState().setLightTheme(theme);
}
})
.catch(() => {
/* empty */
});
.catch(console.log);
}, 1000);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -151,7 +144,7 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
return (
<I18nProvider i18n={i18n}>
<Element {...props} />
<Element />
</I18nProvider>
);
};

View File

@@ -126,6 +126,7 @@ export async function setAppLockVerificationCipher(appLockPassword: string) {
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
DatabaseLogger.error(e);
console.log(e);
}
}
@@ -158,77 +159,79 @@ export function clearDatabaseKey() {
export async function getDatabaseKey(appLockPassword?: string) {
if (DB_KEY) return DB_KEY;
if (appLockPassword) {
const databaseKeyCipher: Cipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
try {
if (appLockPassword) {
const databaseKeyCipher: Cipher =
CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
if (hasKey) {
const credentials = await Keychain.getInternetCredentials(
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
if (hasKey) {
const credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = (credentials as Keychain.UserCredentials).password;
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = (credentials as Keychain.UserCredentials).password;
}
}
}
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key as string;
DB_KEY = derivedDatabaseKey.key as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
)) as Cipher;
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
if (!DB_KEY) {
throw new Error(
`Failed to get database key, ${await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
)}`
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
return DB_KEY;
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
)) as Cipher;
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
return DB_KEY;
} catch (e) {
ToastManager.error(e as Error, "Error getting database key");
console.log(e, "error");
DatabaseLogger.error(e);
return null;
}
}
export async function deriveCryptoKeyFallback(data: SerializedKey) {
@@ -309,6 +312,7 @@ export async function getCryptoKey() {
return key;
} catch (e) {
console.log("getCryptoKey", e);
DatabaseLogger.error(e);
}
}
@@ -335,7 +339,7 @@ export async function hash(
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
if (options?.usesFallback && Platform.OS !== "ios") {
return "";
return null;
}
return (
@@ -346,17 +350,7 @@ export async function hash(
}
export async function generateCryptoKey(password: string, salt?: string) {
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
}
export async function generateCryptoKeyFallback(
password: string,
salt?: string
): Promise<SerializedKey> {
return Sodium.deriveKeyFallback?.(
password,
salt as string
) as Promise<SerializedKey>;
return (await Sodium.deriveKey(password, salt)) as Promise<SerializedKey>;
}
export function getAlgorithm(base64Variant: number) {

View File

@@ -39,6 +39,8 @@ export async function setupDatabase(password?: string) {
const key = await getDatabaseKey(password);
if (!key) throw new Error(strings.databaseSetupFailed());
console.log("Opening database with key:", !!key);
database.host({
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",

View File

@@ -27,8 +27,7 @@ import {
encryptMulti,
generateCryptoKey,
getCryptoKey,
hash,
generateCryptoKeyFallback
hash
} from "./encryption";
import { MMKV } from "./mmkv";
@@ -147,6 +146,5 @@ export const Storage: IStorage = {
decryptMulti,
deriveCryptoKey,
generateCryptoKey,
generateCryptoKeyFallback,
deriveCryptoKeyFallback
};

View File

@@ -27,9 +27,13 @@ import RNFetchBlob from "react-native-blob-util";
*/
export async function compressToBase64(path, type) {
const { width: screenWidth, scale } = Dimensions.get("window");
console.log("COMPRESSING TO BASE64...");
return new Promise((resolve) => {
console.log(path, "image path...");
Image.getSize(path, async (width) => {
console.log("image width", width);
const response = await ImageResizer.createResizedImage(
path,
screenWidth * scale,
@@ -51,12 +55,10 @@ export async function compressToBase64(path, type) {
: response.uri,
"base64"
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
RNFetchBlob.fs
.unlink(response.uri.replace("file://", ""))
.catch(console.log);
resolve(base64);
});
@@ -78,8 +80,6 @@ export async function compressToFile(path, type) {
onlyScaleDown: true
}
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
return response.uri;
}

View File

@@ -24,12 +24,12 @@ import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import filesystem from ".";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import filesystem from "../filesystem";
import { createCacheDir, exists } from "./io";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
@@ -83,9 +83,7 @@ export async function downloadAttachments(attachmentIds: string[]) {
const isCancelled = () => {
if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
useAttachmentStore.getState().setDownloading({
groupId,
current: 0,
@@ -190,13 +188,9 @@ export async function downloadAttachments(attachmentIds: string[]) {
ToastManager.error(e as Error, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
}
}
@@ -327,14 +321,10 @@ export default async function downloadAttachment(
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(() => {
/* empty */
});
.catch(console.log);
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
.catch(() => {
/* empty */
});
.catch(console.log);
}
useAttachmentStore.getState().setDownloading({

View File

@@ -123,9 +123,7 @@ export async function downloadFile(
cancelToken.cancel = async (reason) => {
useAttachmentStore.getState().remove(filename);
request.cancel();
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
};
@@ -144,9 +142,7 @@ export async function downloadFile(
useAttachmentStore.getState().remove(filename);
if (await exists(originalFilePath)) {
await RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
/* empty */
});
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
}
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
@@ -173,12 +169,8 @@ export async function downloadFile(
}
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
DatabaseLogger.error(e, "Download failed: ", {
url
});
@@ -196,7 +188,7 @@ export async function checkAttachment(hash: string) {
try {
const size = await getUploadedFileSize(hash);
console.log("File Size", size);
if (size === -1) return { success: true };
if (size === 0)

View File

@@ -47,8 +47,7 @@ export default {
deleteCacheFileByPath,
getCacheSize,
requestPermission,
checkAndCreateDir,
getUploadedFileSize
checkAndCreateDir
};
export const FileStorage: IFileStorage = {

View File

@@ -60,9 +60,7 @@ export async function readEncrypted<TOutputFormat extends DataFormat>(
return output as Output<TOutputFormat>;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(path).catch(console.log);
DatabaseLogger.error(e);
}
}
@@ -92,9 +90,8 @@ export async function writeEncryptedBase64(
type: "url"
});
RNFetchBlob.fs.unlink(filepath).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(filepath).catch(console.log);
console.log("encrypted file output: ", output);
return {
...output,
@@ -109,9 +106,7 @@ export async function deleteFile(
await createCacheDir();
const localFilePath = cacheDir + `/${filename}`;
if (!requestOptions) {
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(localFilePath).catch(console.log);
return true;
}
@@ -122,9 +117,7 @@ export async function deleteFile(
const status = response.info().status;
const ok = status >= 200 && status < 300;
if (ok) {
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(localFilePath).catch(console.log);
}
return ok;
} catch (e) {
@@ -141,14 +134,10 @@ export async function clearFileStorage() {
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (const file of files) {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(() => {
/* empty */
});
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
}
for (const file of oldCache) {
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(() => {
/* empty */
});
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
}
} catch (e) {
DatabaseLogger.error(e, "clearFileStorage");
@@ -172,19 +161,20 @@ export async function migrateFilesFromCache() {
const migratedFilesPath = cacheDir + "/.migrated_1";
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
if (migrated) {
console.log("Files migrated already");
return;
}
const files = await RNFetchBlob.fs.ls(cacheDir);
console.log("Files to migrate:", files.join(","));
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (const file of oldCache) {
if (file.startsWith("org.") || file.startsWith("com.")) continue;
RNFetchBlob.fs
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
.catch(() => {
/* empty */
});
.catch(console.log);
console.log("Moved", file);
}
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
} catch (e) {
@@ -193,17 +183,13 @@ export async function migrateFilesFromCache() {
}
export async function clearCache() {
await RNFetchBlob.fs.unlink(cacheDir).catch(() => {
/* empty */
});
await RNFetchBlob.fs.unlink(cacheDir).catch(console.log);
await createCacheDir();
eSendEvent("cache-cleared");
}
export async function deleteCacheFileByPath(path: string) {
await RNFetchBlob.fs.unlink(path).catch(() => {
/* empty */
});
await RNFetchBlob.fs.unlink(path).catch(console.log);
}
export async function deleteCacheFileByName(name: string) {
@@ -212,21 +198,15 @@ export async function deleteCacheFileByName(name: string) {
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${name}`;
await RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
/* empty */
});
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(() => {
/* empty */
});
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(console.log);
}
export async function deleteDCacheFiles() {
const files = await RNFetchBlob.fs.ls(cacheDir);
for (const file of files) {
if (file.includes("_dcache") || file.startsWith("NN_")) {
await RNFetchBlob.fs.unlink(file).catch(() => {
/* empty */
});
if (file.includes("_dcache")) {
await RNFetchBlob.fs.unlink(file).catch(console.log);
}
}
}
@@ -265,9 +245,7 @@ export async function exists(filename: string) {
);
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(() => {
/* empty */
});
.catch(console.log);
return false;
}
@@ -303,7 +281,7 @@ export async function getCacheSize() {
await createCacheDir();
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
let total = 0;
console.log("Total files", stat.length);
stat.forEach((file) => {
total += parseInt(file.size as unknown as string);
});

View File

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

View File

@@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function info(context: string, ...logs: unknown[]) {}
function info(context: string, ...logs: unknown[]) {
console.log(`${new Date().toLocaleDateString()}::info::${context}:`, ...logs);
}
function error(context: string, ...logs: unknown[]) {
console.log(

View File

@@ -41,9 +41,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
await sleep(500);
}
if (item.type === "link") {
Linking.openURL(item.data).catch(() => {
/* empty */
});
Linking.openURL(item.data).catch(console.log);
} else if (item.type === "promo") {
presentSheet({
component: (

View File

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

View File

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

View File

@@ -528,9 +528,7 @@ export const AttachmentDialog = ({
setAttachments(results);
setLoading(false);
})
.catch(() => {
/* empty */
});
.catch(console.log);
}}
/>
)

View File

@@ -77,6 +77,7 @@ export const ChangePassword = () => {
throw new Error(strings.backupFailed() + `: ${result.error}`);
}
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastManager.show({
heading: strings.passwordChangedSuccessfully(),

View File

@@ -70,7 +70,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
switch (step) {
case LoginSteps.emailAuth: {
const mfaInfo = await db.user.authenticateEmail(email.current);
console.log("email auth", mfaInfo);
if (mfaInfo) {
TwoFactorVerification.present(
async (mfa, callback) => {

View File

@@ -38,7 +38,7 @@ import { useAppState } from "../../hooks/use-app-state";
export interface BaseDialogProps extends PropsWithChildren {
animation?: "fade" | "none" | "slide" | undefined;
visible?: boolean;
visible: boolean;
onRequestClose?: () => void;
onShow?: () => void;
premium?: boolean;
@@ -75,9 +75,9 @@ const BaseDialog = ({
background
}: BaseDialogProps) => {
const floating = useIsFloatingKeyboard();
const appState = useAppState();
const lockEvents = useRef(false);
const [internalVisible, setIntervalVisible] = useState(true);
const locked = useUserStore((state) => state.appLocked);
useEffect(() => {
return () => {
@@ -86,20 +86,26 @@ const BaseDialog = ({
}, []);
useEffect(() => {
if (locked) {
setIntervalVisible(false);
lockEvents.current = true;
const unsub = useUserStore.subscribe((state) => {
if (!state.appLocked) {
setIntervalVisible(true);
unsub();
setTimeout(() => {
lockEvents.current = false;
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.canLockAppInBackground()) {
if (appState === "background") {
setIntervalVisible(false);
if (useUserStore.getState().appLocked) {
lockEvents.current = true;
const unsub = useUserStore.subscribe((state) => {
if (!state.appLocked) {
setIntervalVisible(true);
unsub();
setTimeout(() => {
lockEvents.current = false;
});
}
});
}
});
}
}
}, [locked]);
}, [appState]);
const Wrapper = useSafeArea ? SafeAreaView : View;

View File

@@ -80,9 +80,8 @@ export const Dialog = ({ context = "global" }) => {
return;
}
}
setChecked(false);
values.current.inputValue = undefined;
setVisible(false);
hide();
};
const show = useCallback(
@@ -105,14 +104,13 @@ export const Dialog = ({ context = "global" }) => {
eUnSubscribeEvent(eOpenSimpleDialog, show);
eUnSubscribeEvent(eCloseSimpleDialog, hide);
};
}, [hide, show]);
}, [show]);
const hide = React.useCallback(() => {
const hide = () => {
setChecked(false);
values.current.inputValue = undefined;
setVisible(false);
dialogInfo.onClose?.();
}, [dialogInfo]);
};
const onNegativePress = async () => {
if (dialogInfo.onClose) {

View File

@@ -180,7 +180,7 @@ AttachImage.present = (response: ImageType[], context?: string) => {
close={close}
onAttach={(result) => {
resolved = true;
console.log("closing");
resolve(result);
close?.();
}}

View File

@@ -179,7 +179,6 @@ const ColorPicker = ({
title: title.current,
colorCode: selectedColor
});
if (!id) return;
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
setVisible(false);

View File

@@ -307,7 +307,9 @@ const PDFPreview = () => {
password={password}
maxScale={6}
onError={onError}
onPressLink={(uri) => {}}
onPressLink={(uri) => {
console.log(`Link pressed: ${uri}`);
}}
style={{
flex: 1,
width: width,

View File

@@ -62,7 +62,7 @@ export default function Progress() {
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
setProgress(options.progress);
cancelCallback.current = options.cancelCallback;
console.log("options", options.fillBackground);
setData({
title: options.title,
paragraph: options.paragraph,

View File

@@ -336,7 +336,7 @@ export class VaultDialog extends Component {
);
});
}
console.log("VAULT UPDATED EVENT");
eSendEvent("vaultUpdated");
this.setState({
loading: false
@@ -406,6 +406,7 @@ export class VaultDialog extends Component {
} else {
await db.vault.add(this.state.note.id);
console.log("update note event...");
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.close();
@@ -542,6 +543,7 @@ export class VaultDialog extends Component {
this.close();
})
.catch((e) => {
console.log("Error", e);
this._takeErrorAction(e);
});
}

View File

@@ -78,10 +78,10 @@ class ExceptionHandler extends React.Component<{
}
export const withErrorBoundry = (Element: React.ElementType, name: string) => {
return function ErrorBoundary(props: any) {
return function ErrorBoundary() {
return (
<ExceptionHandler component={name}>
<Element {...props} />
<Element />
</ExceptionHandler>
);
};

View File

@@ -54,7 +54,7 @@ export const Header = ({
onSearch
}: {
onLeftMenuButtonPress?: () => void;
renderedInRoute?: RouteName;
renderedInRoute: RouteName;
id?: string;
title: string;
headerRightButtons?: HeaderRightButton[];

View File

@@ -41,7 +41,7 @@ export const RightMenus = ({
onSearch
}: {
headerRightButtons?: HeaderRightButton[];
renderedInRoute?: RouteName;
renderedInRoute: RouteName;
id?: string;
onPressDefaultRightButton?: () => void;
search?: boolean;

View File

@@ -41,7 +41,7 @@ export const Title = ({
isHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
renderedInRoute?: string;
renderedInRoute: string;
id?: string;
}) => {
const { colors } = useThemeColors();

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import { DataURL, getFileNameWithExtension } from "@notesnook/core";
import { DataURL } from "@notesnook/core";
import type { ImageAttributes } from "@notesnook/editor";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
@@ -34,11 +34,6 @@ import {
import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy";
import RNFetchBlob from "react-native-blob-util";
import Share from "react-native-share";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useSettingStore } from "../../stores/use-setting-store";
const ImagePreview = () => {
const { colors } = useThemeColors("dialog");
@@ -46,8 +41,6 @@ const ImagePreview = () => {
const [image, setImage] = useState<string>();
const [loading, setLoading] = useState(false);
const imageRef = useRef<ImageAttributes>();
const insets = useGlobalSafeAreaInsets();
const [showHeader, setShowHeader] = useState(true);
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
@@ -80,86 +73,16 @@ const ImagePreview = () => {
silent: true,
cache: true
});
if (!uri) {
setLoading(false);
return;
}
const attachment = await db.attachments.attachment(hash);
const path = `${cacheDir}/${
"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, path).catch(() => {
/* empty */
});
const path = `${cacheDir}/${uri}`;
setImage("file://" + path);
setLoading(false);
}, 100);
};
const close = React.useCallback(() => {
image &&
RNFetchBlob.fs.unlink(image.replace("file://", "")).catch(() => {
/* empty */
});
const close = () => {
setImage(undefined);
setVisible(false);
}, [image]);
const renderHeader = React.useCallback(
() => (
<View
style={{
paddingTop: insets.top,
backgroundColor: "rgba(0,0,0,0.3)",
position: "absolute",
zIndex: 999,
display: showHeader ? "flex" : "none"
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 50,
paddingHorizontal: 12,
gap: 10
}}
>
<IconButton
name="share"
color="white"
style={{
borderWidth: 0
}}
onPress={async () => {
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
await Share.open({
url: image
}).catch(() => {
/* empty */
});
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(false);
}}
/>
<IconButton
name="close"
color="white"
onPress={() => {
close();
}}
/>
</View>
</View>
),
[close, image, insets.top, showHeader]
);
};
return (
visible && (
@@ -167,7 +90,6 @@ const ImagePreview = () => {
background="black"
animation="slide"
visible={true}
useSafeArea={false}
onRequestClose={close}
transparent
>
@@ -208,25 +130,43 @@ const ImagePreview = () => {
/>
</View>
) : (
<>
<ImageViewer
enableImageZoom={true}
renderIndicator={() => <></>}
enableSwipeDown
useNativeDriver
onSwipeDown={close}
saveToLocalByLongPress={false}
onClick={() => {
setShowHeader(!showHeader);
}}
renderHeader={renderHeader}
imageUrls={[
{
url: image as string
}
]}
/>
</>
<ImageViewer
enableImageZoom={true}
renderIndicator={() => <></>}
enableSwipeDown
useNativeDriver
onSwipeDown={close}
saveToLocalByLongPress={false}
renderHeader={() => (
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 50,
paddingHorizontal: 24,
position: "absolute",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.3)",
marginTop: Platform.OS === "android" ? 30 : 0
}}
>
<IconButton
name="close"
color="white"
onPress={() => {
close();
}}
/>
</View>
)}
imageUrls={[
{
url: image as string
}
]}
/>
)}
</View>
</BaseDialog>

View File

@@ -128,6 +128,7 @@ const Intro = ({ navigation }) => {
style={{
width: deviceMode !== "mobile" ? width / 2 : "100%",
backgroundColor: colors.secondary.background,
marginBottom: 20,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
@@ -137,7 +138,7 @@ const Intro = ({ navigation }) => {
marginTop: deviceMode !== "mobile" ? 50 : null,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
minHeight: height * 0.7 - (insets.top + insets.bottom)
minHeight: height * 0.7
}}
>
<SwiperFlatList

View File

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

View File

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

View File

@@ -153,7 +153,9 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
},
items?.cacheItem(index) ? 100 : 0
);
} catch (e) {}
} catch (e) {
console.log("Error", e);
}
})();
}, [index, items, refreshItem]);
@@ -295,6 +297,7 @@ function getDate(item: Item, groupType?: GroupingKey): number {
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},

View File

@@ -119,6 +119,7 @@ export const PricingPlans = ({
setLoading(false);
} catch (e) {
setLoading(false);
console.log("error getting sku", e);
}
}, [promo?.promoCode]);
@@ -128,10 +129,7 @@ export const PricingPlans = ({
if (code.startsWith("com.streetwriters.notesnook")) {
skuId = code;
} else {
skuId = await db.offers?.getCode(
code.split(":")[0],
Platform.OS as "ios" | "android"
);
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
}
const products = await PremiumService.getProducts();
@@ -170,6 +168,7 @@ export const PricingPlans = ({
});
return true;
} catch (e) {
console.log("PROMOCODE ERROR:", code, e);
return false;
}
};
@@ -229,6 +228,7 @@ export const PricingPlans = ({
});
} catch (e) {
setBuying(false);
console.log(e);
}
};

View File

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

View File

@@ -100,6 +100,7 @@ export const AddNotebookSheet = ({
eSendEvent(eOnNotebookUpdated, parent);
if (notebook) {
setImmediate(() => {
console.log(parent, notebook.id);
eSendEvent(eOnNotebookUpdated, notebook.id);
});
}

View File

@@ -90,12 +90,13 @@ export const NotebookItem = ({
for (const key in keys) {
nextState[key] = !state.initialState[key] ? undefined : "deselected";
}
console.log("Single item selection");
state.setSelection({
[item.id]: "selected",
...nextState
});
} else {
console.log("Multi item selection");
state.markAs(item, "selected");
}
};

View File

@@ -134,10 +134,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
) : (
<>
<Input
key="code-input"
fwdRef={codeInputRef}
placeholder={strings.code()}
defaultValue=""
placeholder={strings.verifyNewEmail()}
onChangeText={(code) => {
emailChangeData.current.code = code;
}}

View File

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

View File

@@ -333,17 +333,13 @@ const ExportNotesSheet = ({
if (Platform.OS === "ios") {
Share.open({
url: result?.fileDir + result.fileName
}).catch(() => {
/* empty */
});
}).catch(console.log);
} else {
FileViewer.open(result.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
} as any).catch(() => {
/* empty */
});
} as any).catch(console.log);
}
}}
/>

View File

@@ -62,9 +62,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
}).catch(() => {
/* empty */
});
}).catch(console.log);
}}
/>
</View>

View File

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

View File

@@ -113,9 +113,11 @@ const ManageTagsSheet = (props: {
.sorted()
.then((items) => {
setTags(items);
console.log("searched tags");
});
} else {
db.tags.all.sorted(db.settings.getGroupOptions("tags")).then((items) => {
console.log("items loaded tags", items.placeholders.length);
setTags(items);
});
}

View File

@@ -150,7 +150,7 @@ const PublishNoteSheet = ({
</View>
) : (
<>
{isPublished && publishUrl ? (
{isPublished && (
<View
style={{
flexDirection: "row",
@@ -204,7 +204,7 @@ const PublishNoteSheet = ({
name="content-copy"
/>
</View>
) : null}
)}
<TouchableOpacity
onPress={() => {

View File

@@ -191,17 +191,13 @@ class RecoveryKeySheet extends React.Component {
Share.open({
url: path,
failOnCancel: false
}).catch(() => {
/* empty */
});
}).catch(console.log);
} else {
FileViewer.open(path, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
}).catch(() => {
/* empty */
});
}).catch(console.log);
}
} catch (e) {
console.error(e);

View File

@@ -273,7 +273,7 @@ const ListNoteItem = ({
listType,
noteInternalLinks.length
]);
console.log(noteInternalLinks);
const renderBlock = React.useCallback(
(block: ContentBlock) => (
<ListBlockItem
@@ -419,6 +419,7 @@ export const ReferencesList = ({ item, close }: ReferencesListProps) => {
id={index}
items={items}
onSelect={(note, blockId) => {
console.log(note.id, blockId);
eSendEvent(eOnLoadNote, {
item: note,
blockId: blockId

View File

@@ -556,9 +556,9 @@ export default function ReminderSheet({
{Object.keys(ReminderNotificationModes).map((mode) => (
<Button
key={mode}
title={strings.reminderNotificationModes(
title={strings.reminderNotificationModes[
mode as keyof typeof ReminderNotificationModes
)}
]()}
style={{
marginRight: 12,
borderRadius: 100

View File

@@ -38,7 +38,7 @@ const Sort = ({ type, screen }) => {
);
const updateGroupOptions = async (_groupOptions) => {
const groupType = screen === "Notes" ? "home" : type + "s";
console.log("updateGroupOptions for group", groupType, "in", screen);
await db.settings.setGroupOptions(groupType, _groupOptions);
setGroupOptions(_groupOptions);
setTimeout(() => {

View File

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

View File

@@ -0,0 +1,158 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { ActivityIndicator, DimensionValue, ViewStyle } from "react-native";
import Animated, {
FadeIn,
FadeOut,
Layout,
LightSpeedInLeft
} from "react-native-reanimated";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { SIZE } from "../../../utils/size";
import NativeTooltip from "../../../utils/tooltip";
import { ButtonProps } from "../button";
import { Pressable, useButton } from "../pressable";
import Heading from "../typography/heading";
import Paragraph from "../typography/paragraph";
const AnimatedIcon = Animated.createAnimatedComponent(Icon);
export const AnimatedButton = ({
height = 45,
width = null,
onPress,
loading = false,
title = null,
icon,
fontSize = SIZE.sm,
type = "transparent",
iconSize = SIZE.md,
style = {},
accentColor = "accent",
accentText = "light",
onLongPress,
tooltipText,
textStyle,
iconPosition = "left",
buttonType,
bold,
iconColor,
fwdRef,
...restProps
}: ButtonProps) => {
const { text } = useButton({
type,
accent: accentColor,
text: accentText
});
const textColor = buttonType?.text ? buttonType.text : text;
const Component = bold ? Heading : Paragraph;
return (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
layout={Layout.springify()}
>
<Pressable
{...restProps}
fwdRef={fwdRef}
onPress={onPress}
onLongPress={(event) => {
if (onLongPress) {
onLongPress(event);
return;
}
if (tooltipText) {
NativeTooltip.show(event, tooltipText, NativeTooltip.POSITIONS.TOP);
}
}}
disabled={loading}
type={type}
accentColor={accentColor}
accentText={accentText}
customColor={buttonType?.color}
customSelectedColor={buttonType?.selected}
customOpacity={buttonType?.opacity}
customAlpha={buttonType?.alpha}
style={{
height: height,
width: (width as DimensionValue) || undefined,
paddingHorizontal: 12,
borderRadius: 5,
alignSelf: "center",
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
...(style as ViewStyle)
}}
>
{loading ? (
<ActivityIndicator color={textColor} size={fontSize + 4} />
) : null}
{icon && !loading && iconPosition === "left" ? (
<AnimatedIcon
exiting={FadeOut.duration(100)}
entering={LightSpeedInLeft}
layout={Layout.springify()}
name={icon}
style={{
marginRight: 0
}}
color={iconColor || buttonType?.text || textColor}
size={iconSize}
/>
) : null}
{!title ? null : (
<Component
layout={Layout.springify()}
animated={true}
color={textColor as string}
size={fontSize}
numberOfLines={1}
style={[
{
marginLeft:
icon || (loading && iconPosition === "left") ? 5 : 0,
marginRight:
icon || (loading && iconPosition === "right") ? 5 : 0
},
textStyle
]}
>
{title}
</Component>
)}
{icon && !loading && iconPosition === "right" ? (
<Icon
name={icon}
style={{
marginLeft: 0
}}
color={iconColor || buttonType?.text || textColor}
size={iconSize}
/>
) : null}
</Pressable>
</Animated.View>
);
};

View File

@@ -263,22 +263,20 @@ export const Pressable = ({
const getStyle = useCallback(
({ pressed }: PressableStateCallbackType): ViewStyle | ViewStyle[] => [
{
backgroundColor:
pressed && !disabled
? RGB_Linear_Shade(alpha, hexToRGBA(selectedColor, opacity || 1))
: hexToRGBA(primaryColor, opacity || 1),
backgroundColor: pressed
? RGB_Linear_Shade(alpha, hexToRGBA(selectedColor, opacity || 1))
: hexToRGBA(primaryColor, opacity || 1),
width: "100%",
alignSelf: "center",
borderRadius: noborder ? 0 : br,
justifyContent: "center",
alignItems: "center",
marginBottom: 0,
borderColor:
pressed && !disabled
? customSelectedColor
? getColorLinearShade(customSelectedColor, 0.3, false)
: borderSelectedColor || borderColor
: borderColor || "transparent",
borderColor: pressed
? customSelectedColor
? getColorLinearShade(customSelectedColor, 0.3, false)
: borderSelectedColor || borderColor
: borderColor || "transparent",
borderWidth: borderWidth
},
style,
@@ -300,8 +298,7 @@ export const Pressable = ({
borderColor,
borderWidth,
style,
growFactor,
disabled
growFactor
]
);

View File

@@ -59,9 +59,8 @@ const SheetWrapper = ({
const smallTablet = deviceMode === "smallTablet";
const dimensions = useSettingStore((state) => state.dimensions);
const insets = useGlobalSafeAreaInsets();
const appState = useAppState();
const lockEvents = useRef(false);
const locked = useUserStore((state) => state.appLocked);
let width = dimensions.width > 600 ? 600 : 500;
const style = React.useMemo(() => {
@@ -100,21 +99,24 @@ const SheetWrapper = ({
};
useEffect(() => {
if (locked) {
const ref = fwdRef || localRef;
ref?.current?.hide();
if (useUserStore.getState().appLocked) {
lockEvents.current = true;
const unsub = useUserStore.subscribe((state) => {
if (!state.appLocked) {
ref?.current?.show();
unsub();
lockEvents.current = false;
}
});
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.canLockAppInBackground()) {
if (appState === "background") {
const ref = fwdRef || localRef;
ref?.current?.hide();
if (useUserStore.getState().appLocked) {
lockEvents.current = true;
const unsub = useUserStore.subscribe((state) => {
if (!state.appLocked) {
ref?.current?.show();
unsub();
lockEvents.current = false;
}
});
}
}
}
}, [locked, fwdRef]);
}, [appState, fwdRef]);
return (
<ScopedThemeProvider value="sheet">

View File

@@ -18,15 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { DimensionValue, View } from "react-native";
import { View } from "react-native";
import { SvgXml } from "./lazy";
export const SvgView = ({
width = 250,
height = 250,
src
}: {
width?: DimensionValue;
height?: DimensionValue;
width?: number | string;
height?: number | number;
src?: string;
}) => {
if (!src) return null;

View File

@@ -315,9 +315,7 @@ const trialstarted: { id: string; steps: TStep[] } = {
actionButton: {
text: strings.joinDiscord(),
action: () => {
Linking.openURL("https://discord.gg/zQBK97EE22").catch(() => {
/* empty */
});
Linking.openURL("https://discord.gg/zQBK97EE22").catch(console.log);
}
}
}
@@ -368,9 +366,7 @@ const Support = () => {
width: "90%"
}}
onPress={() => {
Linking.openURL("https://discord.gg/zQBK97EE22").catch(() => {
/* empty */
});
Linking.openURL("https://discord.gg/zQBK97EE22").catch(console.log);
}}
icon="discord"
type="secondary"
@@ -384,9 +380,7 @@ const Support = () => {
width: "90%"
}}
onPress={() => {
Linking.openURL("https://t.me/notesnook").catch(() => {
/* empty */
});
Linking.openURL("https://t.me/notesnook").catch(console.log);
}}
icon="telegram"
type="secondary"

View File

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

View File

@@ -17,15 +17,9 @@ 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 {
EV,
EVENTS,
EventManagerSubscription,
SYNC_CHECK_IDS,
SyncStatusEvent,
User
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { User } from "@notesnook/core";
import { EV, EVENTS, SYNC_CHECK_IDS, SyncStatusEvent } from "@notesnook/core";
import { EventManagerSubscription } from "@notesnook/core";
import notifee from "@notifee/react-native";
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
import React, { useCallback, useEffect, useRef } from "react";
@@ -47,24 +41,17 @@ 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 { endProgress, startProgress } from "../components/dialogs/progress";
import Migrate from "../components/sheets/migrate";
import NewFeature from "../components/sheets/new-feature";
import ReminderSheet from "../components/sheets/reminder";
import { Walkthrough } from "../components/walkthroughs";
import {
resetTabStore,
useTabStore
} from "../screens/editor/tiptap/use-tab-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import {
clearAppState,
editorController,
editorState,
setAppState
editorState
} from "../screens/editor/tiptap/utils";
import { useDragState } from "../screens/settings/editor/state";
import BackupService from "../services/backup";
import BiometricService from "../services/biometrics";
import {
ToastManager,
eSendEvent,
@@ -79,17 +66,14 @@ import {
setRecoveryKeyMessage,
setUpdateAvailableMessage
} from "../services/message";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
import PremiumService from "../services/premium";
import SettingsService from "../services/settings";
import Sync from "../services/sync";
import { clearAllStores, initAfterSync } from "../stores";
import { refreshAllStores } from "../stores/create-db-collection-store";
import { initAfterSync } from "../stores";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { useMessageStore } from "../stores/use-message-store";
import { useSettingStore } from "../stores/use-setting-store";
import { changeSystemBarColors } from "../stores/use-theme-store";
import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/colors";
import { BETA } from "../utils/constants";
@@ -99,13 +83,17 @@ import {
eLoginSessionExpired,
eOnLoadNote,
eOpenAnnouncementDialog,
eUnlockNote,
eUserLoggedIn,
refreshNotesPage
} from "../utils/events";
import { getGithubVersion } from "../utils/github-version";
import { tabBarRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import { NotesnookModule } from "../utils/notesnook-module";
import { changeSystemBarColors } from "../stores/use-theme-store";
import { strings } from "@notesnook/intl";
import { endProgress, startProgress } from "../components/dialogs/progress";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -160,7 +148,6 @@ const onUserSessionExpired = async () => {
const onAppOpenedFromURL = async (event: { url: string }) => {
const url = event.url;
try {
if (url.startsWith("https://app.notesnook.com/account/verified")) {
await onUserEmailVerified();
@@ -170,25 +157,6 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
eSendEvent(eOnLoadNote, { newNote: true });
tabBarRef.current?.goToPage(1, false);
return;
} else if (url.startsWith("https://notesnook.com/open_note")) {
const id = new URL(url).searchParams.get("id");
if (id) {
const note = await db.notes.note(id);
if (note) {
eSendEvent(eOnLoadNote, {
item: note
});
tabBarRef.current?.goToPage(1, false);
}
}
} else if (url.startsWith("https://notesnook.com/open_reminder")) {
const id = new URL(url).searchParams.get("id");
if (id) {
const reminder = await db.reminders.reminder(id);
if (reminder) ReminderSheet.present(reminder);
}
} else if (url.startsWith("https://notesnook.com/new_reminder")) {
ReminderSheet.present();
}
} catch (e) {
console.error(e);
@@ -240,20 +208,11 @@ const onRequestPartialSync = async (
};
const onLogout = async (reason: string) => {
DatabaseLogger.log("User Logged Out " + reason);
setLoginMessage();
await PremiumService.setPremiumStatus();
await BiometricService.resetCredentials();
MMKV.clearStore();
resetTabStore();
clearAllStores();
setImmediate(() => {
refreshAllStores();
DatabaseLogger.log("User Logged Out" + reason);
Notifications.setupReminders(true);
SettingsService.set({
introCompleted: true
});
Navigation.queueRoutesForUpdate();
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
};
async function checkForShareExtensionLaunchedInBackground() {
@@ -278,7 +237,9 @@ async function checkForShareExtensionLaunchedInBackground() {
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
MMKV.removeItem("shareExtensionOpened");
}
} catch (e) {}
} catch (e) {
console.log(e);
}
}
async function saveEditorState() {
@@ -519,14 +480,12 @@ export const useAppEvents = () => {
}
if (fullBackup) {
await BackupService.run(false, undefined, "full");
await BackupService.run(true, undefined, "full");
}
}
if (SettingsService.getProperty("offlineMode")) {
db.attachments.cacheAttachments().catch(() => {
/* empty */
});
db.attachments.cacheAttachments().catch(console.log);
}
}
}, []);
@@ -574,6 +533,7 @@ export const useAppEvents = () => {
});
}
} catch (e) {
console.log(e);
ToastManager.error(e as Error, "Error updating user", "global");
}
@@ -621,8 +581,7 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.migrationStarted, (name) => {
if (
name !== "notesnook" ||
!SettingsService.getProperty("introCompleted") ||
Config.isTesting === "true"
!SettingsService.getProperty("introCompleted")
)
return;
startProgress({
@@ -635,8 +594,7 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.migrationFinished, (name) => {
if (
name !== "notesnook" ||
!SettingsService.getProperty("introCompleted") ||
Config.isTesting === "true"
!SettingsService.getProperty("introCompleted")
)
return;
endProgress();
@@ -644,17 +602,22 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.vaultLocked, async () => {
// Lock all notes in all tabs...
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
if (!noteId) continue;
const note = await db.notes.note(noteId);
const locked = note && (await db.vaults.itemExists(note));
if (locked) {
useTabStore.getState().updateTab(tab.id, {
session: {
locked: true,
noteLocked: true
}
locked: true
});
if (
tab.id === useTabStore.getState().currentTab &&
locked &&
!editorState().movedAway
) {
// Show unlock note screen.
eSendEvent(eUnlockNote);
}
}
}
}),
@@ -663,7 +626,6 @@ export const useAppEvents = () => {
const emitterSubscriptions = [
Linking.addEventListener("url", onAppOpenedFromURL),
SodiumEventEmitter.addListener(
"onSodiumProgress",
onFileEncryptionProgress
@@ -749,15 +711,13 @@ export const useAppEvents = () => {
setTimeout(() => {
sub = AppState.addEventListener("change", onAppStateChanged);
if (
refValues.current.initialUrl &&
!refValues.current.initialUrl?.includes("open_note")
refValues.current.initialUrl?.startsWith(
"https://app.notesnook.com/account/verified"
)
) {
onAppOpenedFromURL({
url: refValues.current.initialUrl!
});
onUserEmailVerified();
}
}, 1000);
refValues.current.removeInternetStateListener = NetInfo.addEventListener(
onInternetStateChanged
);
@@ -800,7 +760,9 @@ export const useAppEvents = () => {
useEffect(() => {
if (!appLocked && isAppLoading) {
initializeLogger()
.catch((e) => {})
.catch((e) => {
console.log(e);
})
.finally(() => {
//@ts-ignore
initializeDatabase();

View File

@@ -32,11 +32,18 @@ export function useAppState() {
const subscription = AppState.addEventListener("change", onChange);
return () => {
subscription.remove();
// @ts-expect-error - React Native >= 0.65
if (typeof subscription?.remove === "function") {
// @ts-expect-error - need update @types/react-native@0.65.x
subscription.remove();
} else {
// React Native < 0.65
AppState.removeEventListener("change", onChange);
}
};
}, []);
return appState;
}
export type { AppStateStatus };
export { AppStateStatus };

View File

@@ -196,6 +196,7 @@ export const useTotalNotes = (type: "notebook" | "tag" | "color") => {
}
setTotalNotesById(totalNotesById);
});
console.log("useTotalNotes.getTotalNotes");
},
[type]
);

View File

@@ -31,7 +31,7 @@ export function useGroupOptions(type: any) {
const onUpdate = (groupType: string) => {
if (groupType !== type) return;
const options = db.settings?.getGroupOptions(type) as any;
console.log("useGroupOptions.onUpdate", type, options);
if (
groupOptions?.groupBy !== options.groupBy ||
groupOptions?.sortBy !== options.sortBy ||

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
import { Keyboard, KeyboardEventListener, KeyboardMetrics } from "react-native";
import { Keyboard, KeyboardEventListener, ScreenRect } from "react-native";
const emptyCoordinates = Object.freeze({
screenX: 0,
@@ -34,8 +34,8 @@ const initialValue = {
export default function useKeyboard() {
const [shown, setShown] = useState(false);
const [coordinates, setCoordinates] = useState<{
start: undefined | KeyboardMetrics;
end: KeyboardMetrics;
start: undefined | ScreenRect;
end: ScreenRect;
}>(initialValue);
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);

View File

@@ -74,8 +74,10 @@ export const useNotebook = (
};
const onUpdate = (type: string) => {
console.log("event", type);
if (type !== "notebooks") return;
onRequestUpdate();
console.log("useNotebook.onUpdate", item?.id, Date.now());
};
eSubscribeEvent(eGroupOptionsUpdated, onUpdate);

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
@@ -45,6 +44,7 @@ import useNavigationStore from "../stores/use-navigation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { useSettingStore } from "../stores/use-setting-store";
import { rootNavigatorRef } from "../utils/global-refs";
import { strings } from "@notesnook/intl";
const NativeStack = createNativeStackNavigator();
const IntroStack = createNativeStackNavigator();
@@ -76,12 +76,11 @@ const _Tabs = () => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const height = useSettingStore((state) => state.dimensions.height);
const insets = useGlobalSafeAreaInsets();
const screenHeight = height - (50 + insets.top + insets.bottom);
React.useEffect(() => {
setTimeout(async () => {
setTimeout(() => {
useNavigationStore.getState().update(homepage);
}, 1000);
}, [homepage]);

View File

@@ -97,7 +97,7 @@ const _TabsHolder = () => {
tabBarRef.current?.goToPage(1, false);
return;
}
if (item && item.type === "notesnook.action.newnote") {
if (item.type === "notesnook.action.newnote") {
clearAppState();
if (!tabBarRef.current) {
await sleep(3000);
@@ -509,8 +509,7 @@ const onChangeTab = async (event) => {
});
} else {
if (
useTabStore.getState().getTab(useTabStore.getState().currentTab).session
?.locked
useTabStore.getState().getTab(useTabStore.getState().currentTab).locked
) {
eSendEvent(eUnlockNote);
}
@@ -524,15 +523,13 @@ const onChangeTab = async (event) => {
// Lock all tabs with locked notes...
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
if (!noteId) continue;
const note = await db.notes.note(noteId);
const locked = note && (await db.vaults.itemExists(note));
if (locked) {
useTabStore.getState().updateTab(tab.id, {
session: {
locked: true
}
locked: true
});
}
}

View File

@@ -9,13 +9,13 @@
"@streetwriters/showdown": "^3.0.1-alpha.2",
"absolutify": "^0.1.0",
"buffer": "^6.0.3",
"dayjs": "^1.10.4",
"deprecated-react-native-prop-types": "^4.1.0",
"entities": "^3.0.1",
"fflate": "^0.7.3",
"html-to-text": "9.0.5",
"phone": "^3.1.14",
"qclone": "^1.2.0",
"dayjs": "^1.11.13",
"react-native-actions-sheet": "0.9.7",
"react-native-drax": "^0.10.2",
"react-native-image-zoom-viewer": "^3.0.1",
@@ -36,15 +36,10 @@
"@streetwriters/kysely": "^0.27.4",
"pathe": "1.1.2",
"react-native-format-currency": "0.0.5",
"@lingui/core": "5.1.2",
"@lingui/react": "5.1.2",
"@lingui/react": "4.11.2",
"@lingui/core": "4.11.2",
"react-native-check-version": "^1.3.0",
"react-native-material-menu": "^2.0.0",
"@trpc/client": "^10.45.2",
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"@tanstack/react-query": "^4.36.1",
"async-mutex": "0.5.0"
"react-native-material-menu": "^2.0.0"
},
"sideEffects": false
}

View File

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

View File

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

View File

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

View File

@@ -16,8 +16,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export const EditorEvents = {
export const EventTypes = {
selection: "editor-event:selection",
content: "editor-event:content",
title: "editor-event:title",
@@ -50,9 +49,5 @@ export const EditorEvents = {
disableReadonlyMode: "editor-events:disable-readonly-mode",
readonlyEditorLoaded: "readonlyEditorLoaded",
error: "editorError",
dbLogger: "editor-events:dbLogger",
goBack: "editor-events:go-back",
goForward: "editor-events:go-forward",
saveScroll: "editor-events:save-scroll",
newNote: "editor-events:new-note"
} as const;
dbLogger: "editor-events:dbLogger"
};

View File

@@ -43,7 +43,6 @@ import { eCloseSheet } from "../../../utils/events";
import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
import { strings } from "@notesnook/intl";
import { useUserStore } from "../../../stores/use-user-store";
const showEncryptionSheet = (file: DocumentPickerResponse) => {
presentSheet({
@@ -61,7 +60,7 @@ const santizeUri = (uri: string) => {
type PickerOptions = {
noteId?: string;
tabId?: string;
tabId?: number;
type: "image" | "camera" | "file";
reupload: boolean;
hash?: string;
@@ -132,6 +131,7 @@ const file = async (fileOptions: PickerOptions) => {
if (
fileOptions.tabId !== undefined &&
fileOptions.noteId &&
useTabStore.getState().getNoteIdForTab(fileOptions.tabId) ===
fileOptions.noteId
) {
@@ -194,7 +194,9 @@ const camera = async (options: PickerOptions) => {
options
);
})
.catch((e) => {});
.catch((e) => {
console.log("camera error: ", e);
});
} catch (e) {
ToastManager.show({
heading: (e as Error).message,
@@ -222,7 +224,9 @@ const gallery = async (options: PickerOptions) => {
options
)
)
.catch((e) => {});
.catch((e) => {
console.log("gallery error: ", e);
});
} catch (e) {
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
ToastManager.show({
@@ -230,19 +234,13 @@ const gallery = async (options: PickerOptions) => {
type: "error",
context: "global"
});
console.log("attachment error:", e);
}
};
const pick = async (options: PickerOptions) => {
if (!PremiumService.get()) {
const user = await db.user.getUser();
if (!user) {
ToastManager.show({
heading: strings.loginRequired(),
type: "error"
});
return;
}
if (editorState().isFocused) {
editorState().isFocused = true;
}
@@ -253,7 +251,6 @@ const pick = async (options: PickerOptions) => {
}
return;
}
useUserStore.getState().setDisableAppLockRequests(true);
if (options?.type.startsWith("image") || options?.type === "camera") {
if (options.type.startsWith("image")) {
gallery(options);
@@ -270,7 +267,6 @@ const handleImageResponse = async (
options: PickerOptions
) => {
const result = await AttachImage.present(response, options.context);
if (!result) return;
const compress = result.compress;
@@ -314,14 +310,18 @@ const handleImageResponse = async (
? fileName.replace(/HEIC|HEIF/, "jpeg")
: fileName;
console.log("attaching image...", fileName);
console.log("attaching file...");
if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
console.log("attaching image to note...");
if (
options.tabId !== undefined &&
useTabStore.getState().getNoteIdForTab(options.tabId) === options.noteId
) {
console.log("attaching image to note...");
editorController.current?.commands.insertImage(
{
hash: hash,
@@ -388,6 +388,7 @@ export async function attachFile(
encryptionInfo.mimeType = type;
encryptionInfo.filename = filename;
encryptionInfo.alg = "xcha-stream";
encryptionInfo.size = encryptionInfo.length;
encryptionInfo.key = key;
if (options?.reupload && exists) {
const attachment = await db.attachments.attachment(hash);
@@ -396,7 +397,6 @@ export async function attachFile(
} else {
encryptionInfo = { hash: hash };
}
await db.attachments.add(encryptionInfo);
return true;
} catch (e) {

View File

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

View File

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

View File

@@ -32,11 +32,8 @@ import {
isEncryptedContent,
isTrashItem
} from "@notesnook/core";
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { strings } from "@notesnook/intl";
import { useThemeEngineStore } from "@notesnook/theme";
import { Mutex } from "async-mutex";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import WebView from "react-native-webview";
import { DatabaseLogger, db } from "../../../common/database";
@@ -49,7 +46,6 @@ import {
eUnSubscribeEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { NotePreviewWidget } from "../../../services/note-preview-widget";
import Notifications from "../../../services/notifications";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
@@ -66,10 +62,12 @@ import { sleep } from "../../../utils/time";
import { unlockVault } from "../../../utils/unlock-vault";
import { onNoteCreated } from "../../notes/common";
import Commands from "./commands";
import { EventTypes } from "./editor-events";
import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types";
import { TabSessionItem, syncTabs, useTabStore } from "./use-tab-store";
import { syncTabs, useTabStore } from "./use-tab-store";
import {
EditorEvents,
clearAppState,
defaultState,
getAppState,
@@ -77,63 +75,11 @@ import {
isEditorLoaded,
post
} from "./utils";
import { Linking } from "react-native";
const loadNoteMutex = new Mutex();
type NoteWithContent = Note & {
content?: NoteContent<false>;
};
type LocalTabStateT = {
editedAt: number;
lastFocusedAt: number;
};
class LocalTabState {
state: Record<string, LocalTabStateT> = {};
noteEditedTime: Record<string, number> = {};
setEditTime(noteId: string, time: number) {
this.noteEditedTime[noteId] = time;
}
get(tabId: string) {
return this.state[tabId] || {};
}
set(tabId: string, state: Partial<LocalTabStateT>) {
this.state[tabId] = {
...this.state[tabId],
...state
};
}
clear(tabId: string) {
delete this.state[tabId];
}
reset() {
this.state = {};
}
needsRefresh(tabId: string, locked: boolean, readonly: boolean) {
const state = this.get(tabId);
const tabSession = useTabStore.getState().getTab(tabId)?.session;
const noteId = useTabStore.getState().getNoteIdForTab(tabId);
if (
tabSession?.locked !== locked ||
tabSession?.readonly !== readonly ||
!noteId
) {
return true;
}
return !state.editedAt || state.editedAt < this.noteEditedTime[noteId];
}
}
export const useEditor = (
editorId = "",
readonly?: boolean,
@@ -152,6 +98,7 @@ export const useEditor = (
isPreview?: boolean;
};
})
| null
| undefined
>
>({});
@@ -171,17 +118,15 @@ export const useEditor = (
const lastContentChangeTime = useRef<Record<string, number>>({});
const lock = useRef(false);
const currentLoadingNoteId = useRef<string>();
const lastTabFocused = useRef<string>();
const localTabState = useRef<LocalTabState>(new LocalTabState());
const loadingState = useRef<string>();
const lastTabFocused = useRef(0);
const blockIdRef = useRef<string>();
const postMessage = useCallback(
async <T>(type: string, data: T, tabId?: string, waitFor = 300) =>
async <T>(type: string, data: T, tabId?: number, waitFor = 300) =>
await post(
editorRef,
sessionIdRef.current,
tabId || useTabStore.getState().currentTab!,
typeof tabId !== "number" ? useTabStore.getState().currentTab : tabId,
type,
data,
waitFor
@@ -196,7 +141,7 @@ export const useEditor = (
}, [commands, insets, isDefaultEditor]);
useEffect(() => {
postMessage(NativeEvents.theme, theme);
postMessage(EditorEvents.theme, theme);
}, [theme, postMessage]);
useEffect(() => {
@@ -206,14 +151,15 @@ export const useEditor = (
}, [commands, tags]);
useEffect(() => {
const event = eSubscribeEvent(eEditorTabFocused, (tabId: string) => {
const event = eSubscribeEvent(eEditorTabFocused, (tabId) => {
console.log("Editot tab focus changed", lastTabFocused.current, tabId);
if (lastTabFocused.current !== tabId) lock.current = false;
lastTabFocused.current = tabId;
lastTabFocused.current = tabId as number;
});
return () => {
event?.unsubscribe();
};
}, []);
});
const overlay = useCallback(
(show: boolean, data = { type: "new" }) => {
@@ -242,11 +188,12 @@ export const useEditor = (
);
const reset = useCallback(
async (tabId: string, resetState = true, resetContent = true) => {
async (tabId: number, resetState = true, resetContent = true) => {
console.log("Resetting tab:", tabId);
const noteId = useTabStore.getState().getNoteIdForTab(tabId);
if (noteId) {
currentNotes.current?.id && db.fs().cancel(noteId);
currentNotes.current[noteId] = undefined;
currentNotes.current[noteId] = null;
currentContents.current[noteId] = null;
editorSessionHistory.clearSession(noteId);
lastContentChangeTime.current[noteId] = 0;
@@ -254,11 +201,18 @@ export const useEditor = (
}
saveCount.current = 0;
currentLoadingNoteId.current = undefined;
loadingState.current = undefined;
lock.current = false;
resetContent && postMessage(NativeEvents.title, "", tabId);
resetContent && postMessage(EditorEvents.title, "", tabId);
resetContent && (await commands.clearContent(tabId));
resetContent && (await commands.clearTags(tabId));
useTabStore.getState().updateTab(tabId, {
noteId: undefined,
locked: false,
noteLocked: false,
readonly: false
});
},
[commands, editorSessionHistory, postMessage]
);
@@ -278,16 +232,6 @@ export const useEditor = (
try {
if (id && !(await db.notes?.note(id))) {
await reset(tabId);
useTabStore.getState().updateTab(tabId, {
session: {
noteId: undefined,
noteLocked: undefined,
locked: undefined,
readonly: undefined,
scrollTop: undefined,
selection: undefined
}
});
return;
}
let note = id ? await db.notes?.note(id) : undefined;
@@ -326,6 +270,13 @@ export const useEditor = (
};
}
// If note is edited, the tab becomes a persistent tab automatically.
if (useTabStore.getState().getTab(tabId)?.previewTab) {
useTabStore.getState().updateTab(tabId, {
previewTab: false
});
}
let saved = false;
setTimeout(() => {
if (saved) return;
@@ -360,9 +311,7 @@ export const useEditor = (
}
useTabStore.getState().updateTab(tabId, {
session: {
noteId: id
}
noteId: id
});
const defaultNotebook = db.settings.getDefaultNotebook();
@@ -377,7 +326,7 @@ export const useEditor = (
if (!noteData.title) {
postMessage(
NativeEvents.title,
EditorEvents.title,
currentNotes.current[id]?.title,
tabId
);
@@ -437,19 +386,12 @@ export const useEditor = (
id === useTabStore.getState().getCurrentNoteId() &&
pendingChanges
) {
postMessage(NativeEvents.title, title || note?.title, tabId);
postMessage(NativeEvents.html, data, tabId);
postMessage(EditorEvents.title, title || note?.title, tabId);
postMessage(EditorEvents.html, data, tabId);
currentNotes.current[id] = note;
}
saveCount.current++;
clearTimeout(timers.current.onsave);
timers.current.onsave = setTimeout(async () => {
if (!id || !note) return;
NotePreviewWidget.updateNote(id, note);
}, 500);
return id;
} catch (e) {
console.error(e);
@@ -487,222 +429,183 @@ export const useEditor = (
);
const loadNote = useCallback(
(event: {
async (event: {
item?: Note;
forced?: boolean;
newNote?: boolean;
tabId?: string;
tabId?: number;
blockId?: string;
session?: TabSessionItem;
newTab?: boolean;
refresh?: boolean;
presistTab?: boolean;
}) => {
loadNoteMutex.runExclusive(async () => {
if (!event) return;
if (event.blockId) {
blockIdRef.current = event.blockId;
if (!event) return;
console.log(event.item?.id, event?.item?.title, "loading note...");
if (event.blockId) {
blockIdRef.current = event.blockId;
}
state.current.currentlyEditing = true;
if (
!state.current.ready &&
(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab
))
) {
state.current.ready = true;
}
if (event.newNote) {
useTabStore.getState().focusEmptyTab();
const tabId = useTabStore.getState().currentTab;
currentNotes.current && (await reset(tabId));
setTimeout(() => {
if (state.current?.ready && !state.current.movedAway)
commands.focus(tabId);
});
} else {
if (!event.item) {
overlay(false);
return;
}
console.log("LOADING NOTE", event.item.id);
const item = event.item;
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab);
if (currentTab?.previewTab && item.id !== currentTab.noteId) {
await commands.setLoading(true, useTabStore.getState().currentTab);
}
const isLockedNote = await db.vaults.itemExists(
event.item as ItemReference
);
const tabLocked =
isLockedNote && !(event.item as NoteWithContent).content;
// If note was already opened in a tab, focus that tab.
if (typeof event.tabId !== "number") {
if (useTabStore.getState().hasTabForNote(event.item.id)) {
const tabId = useTabStore.getState().getTabForNote(event.item.id);
if (typeof tabId === "number") {
useTabStore.getState().updateTab(tabId, {
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote
});
useTabStore.getState().focusTab(tabId);
setTimeout(() => {
if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
}, 150);
}
console.log("Note already loaded, focusing the tab");
} else {
if (event.presistTab) {
// Open note in new tab.
useTabStore.getState().newTab({
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote,
noteId: event.item.id,
previewTab: false
});
console.log("Opening note in new tab");
} else {
console.log("Opening note in preview tab");
// Otherwise we focus the preview tab or create one to open the note in.
useTabStore.getState().focusPreviewTab(event.item.id, {
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote
});
}
}
} else {
if (lastTabFocused.current !== event.tabId) {
useTabStore.getState().focusTab(event.tabId);
}
}
const tabId = event.tabId || useTabStore.getState().currentTab;
if (lastTabFocused.current !== tabId) {
// if ((await waitForEvent(eEditorTabFocused, 1000)) !== tabId) {
// console.log("tab id did not match after focus in 1000ms");
// return;
// }
currentLoadingNoteId.current = item.id;
console.log("Waiting for tab to focus");
return;
}
state.current.movedAway = false;
state.current.currentlyEditing = true;
if (!tabLocked) {
await loadContent(item);
}
if (
!state.current.ready &&
(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab!
))
currentNotes.current[item.id] &&
loadingState.current &&
currentContents.current[item.id]?.data &&
loadingState.current === currentContents.current[item.id]?.data
) {
state.current.ready = true;
// If note is already loading, return.
console.log("Note is already loading...");
return;
}
if (event.newNote && !currentLoadingNoteId.current) {
let tabId;
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab as string);
if (useTabStore.getState().tabs.length === 0 || currentTab?.pinned) {
tabId = useTabStore.getState().newTab();
} else {
tabId = useTabStore.getState().currentTab;
await reset(tabId!, true, true);
if (
event.session?.noteId ||
useTabStore.getState().getTab(tabId!)?.session?.noteId
) {
useTabStore.getState().newTabSession(tabId!, {});
}
}
setTimeout(() => {
if (state.current?.ready && !state.current.movedAway)
commands.focus(tabId!);
});
} else {
if (!event.item) {
overlay(false);
return;
}
const item = event.item;
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab as string);
// If note is already open in a tab, focus that tab.
if (useTabStore.getState().hasTabForNote(item.id) && !event.newTab) {
const tabId = useTabStore.getState().getTabForNote(item.id);
if (
currentTab?.session?.noteId !== item.id &&
tabId !== useTabStore.getState().currentTab
) {
useTabStore.getState().focusTab(tabId as string);
return;
}
}
const isLockedNote = await db.vaults.itemExists(
event.item as ItemReference
);
const tabLocked =
isLockedNote && !(event.item as NoteWithContent).content;
const tabId = event.tabId
? event.tabId
: useTabStore.getState().currentTab;
// Check if tab needs to be refreshed.
if (!event.newTab) {
if (
!event.refresh &&
tabId &&
event.item.id === useTabStore.getState().getNoteIdForTab(tabId) &&
!localTabState.current?.needsRefresh(
tabId,
isLockedNote,
item.readonly
)
) {
commands.setLoading(false, tabId);
return;
} else {
localTabState.current?.setEditTime(
item.id,
localTabState.current?.noteEditedTime[item.id] ||
item.dateEdited
);
localTabState.current?.set(tabId!, {
editedAt:
localTabState.current?.noteEditedTime[item.id] ||
item.dateEdited
});
}
}
currentLoadingNoteId.current = item.id;
// Show loading overlay if note is not already loaded.
if (
tabId &&
(event.item?.id !== useTabStore.getState().getNoteIdForTab(tabId) ||
!currentContents.current[event.item.id]?.data)
) {
await commands.setLoading(true, tabId);
}
const session: Partial<TabSessionItem> = event.session || {
noteId: event.item.id
};
session.noteLocked = isLockedNote;
session.locked = tabLocked;
session.readonly = item.readonly;
const tab = useTabStore.getState().getTab(tabId!);
if (
useTabStore.getState().tabs.length === 0 ||
event.newTab ||
(currentTab?.pinned &&
event.item.id !== currentTab?.session?.noteId)
) {
useTabStore.getState().newTab({
session: session
});
return;
} else {
// A new session is created if the note is changed.
// If the note is already opened, the session is updated.
if (
!tab?.session ||
(event.item.id !== tab?.session?.noteId && tab?.session?.noteId)
) {
useTabStore.getState().newTabSession(tabId!, session);
} else {
useTabStore.getState().updateTab(tabId!, {
session: session
});
}
}
if (lastTabFocused.current !== tabId) return;
if (tabBarRef.current?.page() === 2) {
state.current.movedAway = false;
}
state.current.currentlyEditing = true;
if (!tabLocked) {
await loadContent(item);
} else {
commands.focus(tabId!);
}
lastContentChangeTime.current[item.id] = item.dateEdited;
if (!state.current.ready) {
currentNotes.current[item.id] = item;
if (!currentNotes.current[item.id]) return;
editorSessionHistory.newSession(item.id);
await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"),
"Saved",
tabId!
);
await postMessage(NativeEvents.title, item.title, tabId);
overlay(false);
await postMessage(
NativeEvents.html,
{
data: currentContents.current[item.id]?.data || "",
scrollTop: tab?.session?.scrollTop,
selection: tab?.session?.selection
},
tabId,
10000
);
setTimeout(() => {
if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
}, 300);
await commands.setTags(item);
commands.setSettings();
setTimeout(() => {
if (currentLoadingNoteId.current === event.item?.id) {
currentLoadingNoteId.current = undefined;
}
}, 300);
return;
}
postMessage(NativeEvents.theme, theme);
});
lastContentChangeTime.current[item.id] = 0;
currentLoadingNoteId.current = item.id;
currentNotes.current[item.id] = item;
if (!currentNotes.current[item.id]) return;
editorSessionHistory.newSession(item.id);
await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"),
strings.saved(),
tabId
);
await postMessage(EditorEvents.title, item.title, tabId);
overlay(false);
loadingState.current = currentContents.current[item.id]?.data;
await postMessage(
EditorEvents.html,
currentContents.current[item.id]?.data || "",
tabId,
10000
);
setTimeout(() => {
if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
}, 300);
loadingState.current = undefined;
await commands.setTags(item);
commands.setSettings();
setTimeout(() => {
if (currentLoadingNoteId.current === event.item?.id) {
currentLoadingNoteId.current = undefined;
}
}, 300);
}
postMessage(EditorEvents.theme, theme);
},
[
commands,
@@ -710,6 +613,7 @@ export const useEditor = (
loadContent,
overlay,
postMessage,
readonly,
reset,
theme
]
@@ -721,116 +625,97 @@ export const useEditor = (
isLocal?: boolean
) => {
try {
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
await (async () => {
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) {
await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId);
if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) {
console.log("Removing tab");
await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId);
}
return;
}
return;
}
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
const note = data.type === "note" ? data : await db.notes?.note(noteId);
if (!useTabStore.getState().hasTabForNote(noteId)) return;
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
lock.current = true;
const tab = useTabStore.getState().getTab(tabId);
// Handle this case where note was locked on another device and synced.
let locked = note
? await db.vaults.itemExists(note as ItemReference)
: false;
const note =
data.type === "note" ? data : await db.notes?.note(noteId);
if (data.type === "tiptap") {
locked = data.locked;
}
lock.current = true;
useTabStore.getState().forEachNoteTab(noteId, async (tab) => {
const tabId = tab.id;
let didUnlock = false;
// Handle this case where note was locked on another device and synced.
const locked = note
? await db.vaults.itemExists(note as ItemReference)
: false;
if (note) {
if (!locked && tab?.session?.noteLocked) {
if (tab?.session?.noteLocked || tab?.session?.locked) {
if (useTabStore.getState().currentTab !== tabId) {
localTabState.current?.set(tabId, {
editedAt: 0
if (!locked && tab?.noteLocked) {
// Note lock removed.
if (tab.locked) {
if (useTabStore.getState().currentTab === tabId) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
}
didUnlock = true;
useTabStore.getState().updateTab(tabId, {
session: {
} else {
useTabStore.getState().updateTab(tabId, {
locked: false,
noteLocked: false
}
});
}
} else if (!tab?.session?.noteLocked && locked) {
useTabStore.getState().updateTab(tabId, {
session: {
locked: true,
noteLocked: true
});
commands.setLoading(true, tabId);
}
}
} else if (!tab?.noteLocked && locked) {
// Note lock added.
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
commands.clearContent(tabId);
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
localTabState.current?.set(tabId, {
editedAt: 0
});
}
if (
currentNotes.current[noteId]?.title !== note.title ||
didUnlock
) {
postMessage(NativeEvents.title, note.title, tabId);
if (currentNotes.current[noteId]?.title !== note.title) {
postMessage(EditorEvents.title, note.title, tabId);
}
commands.setTags(note);
if (
currentNotes.current[noteId]?.dateEdited !== note.dateEdited ||
didUnlock
) {
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
strings.saved(),
tabId as string
tabId as number
);
}
if (tab.session?.readonly !== note.readonly || didUnlock) {
useTabStore.getState().updateTab(tabId, {
session: {
readonly: note.readonly
}
});
}
useTabStore.getState().updateTab(tabId, {
readonly: note.readonly
});
}
if (data.type === "tiptap" && note && !isLocal) {
if (
lastContentChangeTime.current[noteId] >= data.dateModified &&
!didUnlock
) {
if (lastContentChangeTime.current[noteId] >= data.dateModified) {
return;
}
if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent(data);
if (!decryptedContent) {
useTabStore.getState().updateTab(tab.id, {
session: {
locked: true,
noteLocked: true
}
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
@@ -838,43 +723,26 @@ export const useEditor = (
}
} else {
await postMessage(
NativeEvents.updatehtml,
{
data: decryptedContent.data,
selection: tab.session?.selection,
scrollTop: tab.session?.scrollTop
},
EditorEvents.updatehtml,
decryptedContent.data,
tabId
);
currentContents.current[note.id] = decryptedContent;
}
} else {
const _nextContent = data.data;
if (
_nextContent === currentContents.current[note.id]?.data &&
!didUnlock
) {
if (_nextContent === currentContents.current[note.id]?.data) {
return;
}
lastContentChangeTime.current[note.id] = note.dateEdited;
console.log(tab.session?.selection);
await postMessage(
NativeEvents.updatehtml,
{
data: _nextContent,
selection: tab.session?.selection,
scrollTop: tab.session?.scrollTop
},
tabId
);
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
if (!isEncryptedContent(data)) {
currentContents.current[note.id] =
data as UnencryptedContentItem;
}
}
}
});
})();
} catch (e) {
DatabaseLogger.error(e as Error, "Error when applying sync changes");
} finally {
@@ -910,7 +778,7 @@ export const useEditor = (
content?: string;
type: string;
ignoreEdit: boolean;
tabId: string;
tabId: number;
pendingChanges?: boolean;
}) => {
DatabaseLogger.log(
@@ -939,13 +807,9 @@ export const useEditor = (
if (noteId) {
lastContentChangeTime.current[noteId] = Date.now();
localTabState.current?.setEditTime(noteId, Date.now());
localTabState?.current?.set(tabId, {
editedAt: Date.now()
});
}
if (type === EditorEvents.content && noteId) {
if (type === EventTypes.content && noteId) {
currentContents.current[noteId as string] = {
data: content,
type: "tiptap",
@@ -995,9 +859,7 @@ export const useEditor = (
if (!appState) return;
state.current.isRestoringState = true;
state.current.currentlyEditing = true;
if (tabBarRef.current?.page() === 2) {
state.current.movedAway = false;
}
state.current.movedAway = false;
if (!state.current.editorStateRestored) {
state.current.isRestoringState = true;
@@ -1028,12 +890,9 @@ export const useEditor = (
!(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab!
useTabStore.getState().currentTab
))
) {
localTabState.current?.set(useTabStore.getState().currentTab!, {
editedAt: 0
});
eSendEvent(eEditorReset, "onReady");
return false;
} else {
@@ -1044,63 +903,36 @@ export const useEditor = (
}, [isDefaultEditor, restoreEditorState]);
const onLoad = useCallback(async () => {
const isAppLoading = useSettingStore.getState().isAppLoading;
if (isAppLoading) {
const sub = useSettingStore.subscribe((state) => {
if (!state.isAppLoading) {
sub();
onLoad();
}
});
return;
}
setTimeout(() => {
postMessage(NativeEvents.theme, theme);
postMessage(EditorEvents.theme, theme);
});
commands.setInsets(
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
);
await commands.setSettings();
localTabState.current?.set(useTabStore.getState().currentTab!, {
editedAt: 0
});
if (!state.current.ready) {
state.current.ready = true;
}
if (!state.current?.initialLoadCalled) {
const url = await Linking.getInitialURL();
let noteId = url && new URL(url).searchParams.get("id");
if (noteId) {
const note = await db.notes?.note(noteId);
if (note) {
loadNote({
item: note
});
}
tabBarRef.current?.goToPage(1);
} else {
noteId = useTabStore.getState().getCurrentNoteId() || null;
if (!noteId) {
loadNote({ newNote: true });
if (tabBarRef.current?.page() === 1) {
state.current.currentlyEditing = false;
}
} else {
const note = await db.notes.note(noteId);
if (note) {
loadNote({
item: note
});
}
}
const noteId = useTabStore.getState().getCurrentNoteId();
if (!noteId) {
loadNote({ newNote: true });
if (tabBarRef.current?.page() === 1) {
state.current.currentlyEditing = false;
}
} else if (state.current?.initialLoadCalled) {
const note = currentNotes.current[noteId];
if (note) {
loadNote({
item: note
});
}
state.current.initialLoadCalled = true;
overlay(false);
}
if (!state.current?.initialLoadCalled) {
state.current.initialLoadCalled = true;
}
overlay(false);
}, [
postMessage,
theme,

View File

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

View File

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

View File

@@ -1,169 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Note, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { FlatList, TouchableOpacity, View } from "react-native";
import { db } from "../../common/database";
import { Header } from "../../components/header";
import Input from "../../components/ui/input";
import Paragraph from "../../components/ui/typography/paragraph";
import { useDBItem } from "../../hooks/use-db-item";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useSettingStore } from "../../stores/use-setting-store";
import { NotesnookModule } from "../../utils/notesnook-module";
const NoteItem = (props: {
id: string | number;
items?: VirtualizedGrouping<Note>;
}) => {
const { colors } = useThemeColors();
const [item] = useDBItem(props.id, "note", props.items);
return (
<TouchableOpacity
activeOpacity={0.7}
onPress={() => {
const widgetId = NotesnookModule.getWidgetId();
NotesnookModule.setString(
"appPreview",
String(widgetId),
JSON.stringify(item)
);
setTimeout(() => {
NotesnookModule.saveAndFinish();
});
}}
style={{
flexDirection: "column",
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
justifyContent: "center",
paddingVertical: 12,
minHeight: 45
}}
>
{!item ? null : (
<View
style={{
flexDirection: "row",
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "column"
}}
>
<Paragraph
numberOfLines={1}
style={{
color: colors.primary.paragraph,
fontSize: 15
}}
>
{item.title}
</Paragraph>
</View>
</View>
)}
</TouchableOpacity>
);
};
export const NotePreviewConfigure = () => {
const [items, setItems] = useState<VirtualizedGrouping<Note>>();
const loading = useSettingStore((state) => state.isAppLoading);
const bounceRef = React.useRef<NodeJS.Timeout>();
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
useEffect(() => {
useSettingStore.getState().setDeviceMode("mobile");
if (loading) return;
db.notes.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
setItems(notes);
});
}, [loading]);
const renderItem = React.useCallback(
({ index }: { item: boolean; index: number }) => {
return <NoteItem id={index} items={items} />;
},
[items]
);
return (
<View
style={{
backgroundColor: colors.primary.background,
flex: 1
}}
>
<Header
canGoBack
title="Select a note"
onLeftMenuButtonPress={() => {
NotesnookModule.cancelAndFinish();
}}
/>
<View
style={{
paddingHorizontal: 12,
paddingTop: 6
}}
>
<Input
placeholder="Search for notes"
containerStyle={{
height: 50
}}
onChangeText={(value) => {
bounceRef.current = setTimeout(() => {
if (!value) {
db.notes.all
.sorted(db.settings.getGroupOptions("notes"))
.then((notes) => {
setItems(notes);
});
return;
}
db.lookup
.notes(value)
.sorted()
.then((notes) => {
setItems(notes);
});
}, 500);
}}
/>
<FlatList
data={items?.placeholders}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
renderItem={renderItem}
windowSize={1}
ListFooterComponent={<View style={{ height: 200 }} />}
/>
</View>
</View>
);
};

View File

@@ -284,13 +284,14 @@ NotebookScreen.navigate = async (item: Notebook, canGoBack?: boolean) => {
focusedRouteId == item?.id
) {
// Update the route in place instead
console.log("Updating existing route in place");
eSendEvent(eUpdateNotebookRoute, {
item: item,
title: item.title,
canGoBack: canGoBack
});
} else {
console.log("Pushing new notebook route");
// Push a new route
Navigation.push("Notebook", {
title: item.title,

View File

@@ -59,6 +59,7 @@ ColoredNotes.navigate = (item: Color, canGoBack: boolean) => {
const { focusedRouteId } = useNavigationStore.getState();
if (focusedRouteId === item.id) {
console.log("ColoredNotes.navigate: route already focused for color");
return;
}

View File

@@ -171,6 +171,7 @@ const NotesPage = ({
setLoadingNotes(false);
})
.catch((e) => {
console.log("Error loading notes", params.current?.title, e, e.stack);
setLoadingNotes(false);
});
}

View File

@@ -60,6 +60,7 @@ TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => {
const { focusedRouteId } = useNavigationStore.getState();
if (focusedRouteId === item.id) {
console.log("TaggedNotes.navigate: route already focused for tag");
return;
}

View File

@@ -91,6 +91,7 @@ export const Reminders = ({
/>
<FloatingButton
title={strings.setReminder()}
onPress={() => {
ReminderSheet.present();
}}

View File

@@ -59,38 +59,15 @@ export const Search = ({ route, navigation }: NavigationProps<"Search">) => {
}
try {
setLoading(true);
let results: VirtualizedGrouping<Item> | undefined;
switch (route.params.type) {
case "note":
results = await db.lookup
.notes(query, route.params.items as FilteredSelector<Note>)
.sorted();
break;
case "notebook":
results = await db.lookup.notebooks(query).sorted();
break;
case "tag":
results = await db.lookup.tags(query).sorted();
break;
case "reminder":
results = await db.lookup.reminders(query).sorted();
break;
case "trash":
results = await db.lookup.trash(query).sorted();
break;
case "attachment":
results = await db.lookup.attachments(query).sorted();
break;
default:
results = undefined;
}
if (!results) {
setSearchStatus(strings.noResultsFound(query));
setLoading(false);
return;
}
const type =
route.params.type === "trash"
? "trash"
: ((route.params?.type + "s") as keyof typeof db.lookup);
console.log(`Searching in ${type} for ${query}`);
const results = await db.lookup[type](
query,
route.params.items as FilteredSelector<Note>
).sorted();
console.log(
`Found ${results.placeholders?.length} results for ${query}`

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