mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4db8b0db01 | ||
|
|
3f1614ab9a | ||
|
|
909d8c35f8 | ||
|
|
a73780f0b0 | ||
|
|
286fdbeec0 | ||
|
|
59099552f8 | ||
|
|
0b6013be58 | ||
|
|
2d93a17f34 | ||
|
|
5ef7e39e17 | ||
|
|
f7de4c9084 | ||
|
|
0672c864f7 | ||
|
|
cb3706c150 | ||
|
|
08c6a99336 | ||
|
|
adf961fa08 | ||
|
|
0eb9ae47e9 | ||
|
|
aa706300a4 | ||
|
|
fd183a0e1e | ||
|
|
b27cd4be0f | ||
|
|
9575561832 | ||
|
|
02f96f8d14 | ||
|
|
3cf46e7ad1 | ||
|
|
93551aa0b7 | ||
|
|
d00c35bdaf | ||
|
|
6e6b793568 | ||
|
|
9e064d88c6 | ||
|
|
1658ad8d18 | ||
|
|
c94da3bdf0 | ||
|
|
1f0fc83a7c | ||
|
|
a6af92c330 | ||
|
|
ef93ecc481 | ||
|
|
039709822c | ||
|
|
25c861ce77 | ||
|
|
670f1570b5 | ||
|
|
c93d82d2d6 | ||
|
|
bced20031b | ||
|
|
f8bd089b51 | ||
|
|
2a4d04a26f | ||
|
|
a0c32191f9 | ||
|
|
662d59f60a | ||
|
|
13befe1947 | ||
|
|
21f8b6d557 | ||
|
|
aef53cace9 | ||
|
|
382b5b0240 | ||
|
|
a45b66d449 | ||
|
|
dd67b1a803 | ||
|
|
00568ae839 | ||
|
|
2b2ea5717c | ||
|
|
5672de8565 | ||
|
|
1c70ad8cec | ||
|
|
0c344a2146 | ||
|
|
38f69b514c | ||
|
|
b502f6a08f | ||
|
|
d68c3269d3 |
@@ -40,12 +40,35 @@ Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you can finally start the desktop app:
|
||||
Now you can finally start the desktop app for development:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
To run the app in release mode:
|
||||
|
||||
```bash
|
||||
npm run staging -- --rebuild
|
||||
```
|
||||
|
||||
This will compile and run the app in production mode but it won't generate any packages. To create the final packages, you'll have to run the following commands:
|
||||
|
||||
```bash
|
||||
npm run release -- --rebuild
|
||||
|
||||
# For macOS
|
||||
npx electron-builder --mac dmg --arm64 --x64 -publish never
|
||||
|
||||
# For Linux (AppImage)
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -publish never
|
||||
|
||||
# For Windows
|
||||
npx electron-builder --win --publish never
|
||||
```
|
||||
|
||||
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.
|
||||
|
||||
## Developer guide
|
||||
|
||||
### The tech stack
|
||||
|
||||
1311
apps/desktop/package-lock.json
generated
1311
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.9",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^29.3.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-builder": "^25.0.0-alpha.11",
|
||||
"esbuild": "^0.20.0",
|
||||
"kysely": "^0.27.3",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { app, dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { AutoLaunch } from "../utils/autolaunch";
|
||||
import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
@@ -152,6 +152,10 @@ export const osIntegrationRouter = t.router({
|
||||
await rm(input);
|
||||
}),
|
||||
|
||||
restart: t.procedure.query(() => {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
}),
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
@@ -191,7 +195,10 @@ export const osIntegrationRouter = t.router({
|
||||
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
|
||||
if (windowControlsIconColor) {
|
||||
config.windowControlsIconColor = windowControlsIconColor;
|
||||
if (process.platform === "win32")
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
!config.desktopSettings.nativeTitlebar
|
||||
)
|
||||
globalThis.window?.setTitleBarOverlay({
|
||||
symbolColor: windowControlsIconColor
|
||||
});
|
||||
|
||||
@@ -84,17 +84,21 @@ async function createWindow() {
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
}),
|
||||
|
||||
titleBarStyle: "hidden",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
color: "#00000000",
|
||||
symbolColor: config.windowControlsIconColor
|
||||
},
|
||||
trafficLightPosition: {
|
||||
x: 16,
|
||||
y: 12
|
||||
},
|
||||
...(config.desktopSettings.nativeTitlebar
|
||||
? {}
|
||||
: {
|
||||
titleBarStyle: "hidden",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
color: "#00000000",
|
||||
symbolColor: config.windowControlsIconColor
|
||||
},
|
||||
trafficLightPosition: {
|
||||
x: 16,
|
||||
y: 12
|
||||
}
|
||||
}),
|
||||
|
||||
webPreferences: {
|
||||
zoomFactor: config.zoomFactor,
|
||||
|
||||
@@ -25,7 +25,8 @@ export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
startMinimized: z.boolean().optional(),
|
||||
minimizeToSystemTray: z.boolean().optional(),
|
||||
closeToSystemTray: z.boolean().optional()
|
||||
closeToSystemTray: z.boolean().optional(),
|
||||
nativeTitlebar: z.boolean().optional()
|
||||
});
|
||||
|
||||
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
|
||||
@@ -35,7 +36,8 @@ export const config = {
|
||||
autoStart: false,
|
||||
startMinimized: false,
|
||||
minimizeToSystemTray: false,
|
||||
closeToSystemTray: false
|
||||
closeToSystemTray: false,
|
||||
nativeTitlebar: false
|
||||
},
|
||||
privacyMode: false,
|
||||
isSpellCheckerEnabled: true,
|
||||
|
||||
@@ -69,7 +69,6 @@ const App = () => {
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Platform } from "react-native";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { createCacheDir } from "./io";
|
||||
import { getUploadedFileSize } from "./download";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
@@ -50,9 +51,15 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
|
||||
const uploadUrl = await res.text();
|
||||
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url");
|
||||
|
||||
const uploadUrl = res.ok ? await res.text() : await res.json();
|
||||
|
||||
if (typeof uploadUrl !== "string") {
|
||||
throw new Error(
|
||||
uploadUrl.error || "Unable to resolve attachment upload url."
|
||||
);
|
||||
}
|
||||
|
||||
let uploadFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
@@ -125,8 +132,10 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
return result;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
DatabaseLogger.info(`File upload error: ${filename}, ${e}`);
|
||||
DatabaseLogger.error(e);
|
||||
ToastManager.error(e, "File upload failed");
|
||||
DatabaseLogger.error(e, "File upload failed", {
|
||||
filename
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { AppStateStatus, Platform, TextInput, View } from "react-native";
|
||||
//@ts-ignore
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { DatabaseLogger } from "../../common/database";
|
||||
import {
|
||||
decrypt,
|
||||
@@ -121,7 +120,6 @@ const AppLockedOverlay = () => {
|
||||
);
|
||||
if (unlocked) {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
}
|
||||
biometricUnlockAwaitingUserInput.current = false;
|
||||
@@ -148,7 +146,6 @@ const AppLockedOverlay = () => {
|
||||
}
|
||||
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
@@ -163,10 +160,6 @@ const AppLockedOverlay = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (appState === "active") {
|
||||
enabled(false);
|
||||
}
|
||||
|
||||
const prevState = lastAppState.current;
|
||||
lastAppState.current = appState;
|
||||
|
||||
@@ -192,13 +185,8 @@ const AppLockedOverlay = () => {
|
||||
DatabaseLogger.info("Biometric unlock request");
|
||||
onUnlockAppRequested();
|
||||
}
|
||||
|
||||
enabled(false);
|
||||
} else {
|
||||
SettingsService.appEnteredBackground();
|
||||
if (SettingsService.get().privacyScreen) {
|
||||
enabled(true);
|
||||
}
|
||||
}
|
||||
}, [appState, onUnlockAppRequested, appLocked]);
|
||||
|
||||
|
||||
@@ -355,6 +355,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
|
||||
AttachmentDialog.present = (note?: Note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog note={note} />
|
||||
component: () => <AttachmentDialog note={note} />,
|
||||
keyboardHandlerDisabled: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,12 +17,16 @@ 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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Sync from "../../services/sync";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
@@ -34,11 +38,6 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { useLogin } from "./use-login";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import Sync from "../../services/sync";
|
||||
import { Notice } from "../ui/notice";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -184,7 +183,11 @@ export const Login = ({ changeMode }) => {
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -243,6 +246,7 @@ export const Login = ({ changeMode }) => {
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
|
||||
@@ -37,6 +37,7 @@ import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
|
||||
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -143,7 +144,10 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
@@ -214,6 +218,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
code.current = value;
|
||||
//onNext();
|
||||
}}
|
||||
onSubmitEditing={onNext}
|
||||
caretHidden
|
||||
inputStyle={{
|
||||
fontSize: SIZE.lg,
|
||||
@@ -225,6 +230,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
keyboardType={
|
||||
currentMethod.method === "recoveryCode" ? "default" : "numeric"
|
||||
}
|
||||
enablesReturnKeyAutomatically
|
||||
containerStyle={{
|
||||
height: 60,
|
||||
borderWidth: 0,
|
||||
@@ -297,7 +303,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -23,10 +23,7 @@ import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
|
||||
import { Dialog } from "../dialog";
|
||||
import { Issue } from "../sheets/github/issue";
|
||||
|
||||
const error = (
|
||||
stack: string,
|
||||
component: string
|
||||
) => `Please let us know what happened. What steps we can take to reproduce the issue here.
|
||||
const error = (stack: string, component: string) => `
|
||||
|
||||
_______________________________
|
||||
Stacktrace: In ${component}::${stack}`;
|
||||
@@ -39,9 +36,9 @@ class ExceptionHandler extends React.Component<{
|
||||
error: Error | null;
|
||||
hasError: boolean;
|
||||
} = {
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error: error };
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ export const Card = ({ color }: { color?: string }) => {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 0
|
||||
paddingHorizontal: 0,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -91,7 +92,6 @@ export const Card = ({ color }: { color?: string }) => {
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
flexShrink: 1,
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -18,35 +18,33 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Debug } from "@notesnook/core/dist/api/debug";
|
||||
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Linking, Platform, Text, TextInput, View } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import { useStoredRef } from "../../../hooks/use-stored-ref";
|
||||
import { ToastManager, eSendEvent } from "../../../services/event-manager";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const body = useStoredRef("issueBody", defaultBody);
|
||||
const body = useStoredRef("issueBody");
|
||||
const title = useStoredRef("issueTitle", defaultTitle);
|
||||
|
||||
const [done, setDone] = useState(false);
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const bodyRef = useRef();
|
||||
const initialLayout = useRef(false);
|
||||
const issueUrl = useRef();
|
||||
|
||||
const onPress = async () => {
|
||||
if (loading) return;
|
||||
@@ -56,57 +54,24 @@ export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
let issue_url = await Debug.report({
|
||||
issueUrl.current = await Debug.report({
|
||||
title: title.current,
|
||||
body:
|
||||
body.current +
|
||||
`\n${defaultBody || ""}` +
|
||||
`\n_______________
|
||||
**Device information:**
|
||||
App version: ${getVersion()}
|
||||
Platform: ${Platform.OS}
|
||||
Model: ${Platform.constants.Brand || ""}-${Platform.constants.Model || ""}-${
|
||||
Platform.constants.Version || ""
|
||||
}
|
||||
Device: ${getBrand() || ""}-${getModel() || ""}-${getSystemVersion() || ""}
|
||||
Pro: ${PremiumService.get()}
|
||||
Logged in: ${user ? "yes" : "no"}`,
|
||||
userId: user?.id
|
||||
});
|
||||
setLoading(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
body.reset();
|
||||
title.reset();
|
||||
await sleep(300);
|
||||
presentDialog({
|
||||
title: "Issue reported",
|
||||
paragraph: (
|
||||
<Text>
|
||||
You can track your issue at{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(issue_url);
|
||||
}}
|
||||
>
|
||||
{issue_url}.
|
||||
</Text>{" "}
|
||||
Please note that we will respond to your issue on the given link. We
|
||||
recommend that you save it.
|
||||
</Text>
|
||||
),
|
||||
positiveText: "Copy link",
|
||||
positivePress: () => {
|
||||
Clipboard.setString(issue_url);
|
||||
ToastManager.show({
|
||||
heading: "Issue url copied!",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
},
|
||||
negativeText: "Close"
|
||||
});
|
||||
setDone(true);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
@@ -124,125 +89,177 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={issueTitle || "Report issue"}
|
||||
paragraph={
|
||||
issueTitle
|
||||
? "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
|
||||
: "Let us know if you have faced any issue/bug while using Notesnook."
|
||||
}
|
||||
/>
|
||||
{done ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
height: 250,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Heading>Issue submitted</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
selectable={true}
|
||||
>
|
||||
You can track your issue at{" "}
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(issueUrl.current);
|
||||
}}
|
||||
>
|
||||
{issueUrl.current}
|
||||
</Paragraph>
|
||||
. Please note that we will respond to your issue on the given
|
||||
link. We recommend that you save it.
|
||||
</Paragraph>
|
||||
|
||||
<Seperator half />
|
||||
|
||||
<TextInput
|
||||
placeholder="Title"
|
||||
onChangeText={(v) => (title.current = v)}
|
||||
defaultValue={title.current}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
marginBottom: 10,
|
||||
fontSize: SIZE.md,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
ref={bodyRef}
|
||||
placeholder={`Tell us more about the issue you are facing.
|
||||
|
||||
For example:
|
||||
- Steps to reproduce the issue
|
||||
- Things you have tried etc.`}
|
||||
multiline
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
onChangeText={(v) => (body.current = v)}
|
||||
onLayout={() => {
|
||||
if (initialLayout.current) return;
|
||||
initialLayout.current = true;
|
||||
if (body.current) {
|
||||
bodyRef.current?.setNativeProps({
|
||||
text: body.current,
|
||||
selection: {
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
maxHeight: 200,
|
||||
fontSize: SIZE.sm,
|
||||
marginBottom: 2.5,
|
||||
color: colors.primary.paragraph
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>{`App version: ${getVersion()} Platform: ${Platform.OS} Model: ${
|
||||
Platform.constants.Brand
|
||||
}-${Platform.constants.Model}-${Platform.constants.Version}`}</Paragraph>
|
||||
|
||||
<Seperator />
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={loading ? null : "Submit"}
|
||||
loading={loading}
|
||||
width="100%"
|
||||
type="accent"
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
The information above will be publically available at{" "}
|
||||
<Text
|
||||
onPress={() => {
|
||||
Linking.openURL("https://github.com/streetwriters/notesnook");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
github.com/streetwriters/notesnook.
|
||||
</Text>{" "}
|
||||
If you want to ask something in general or need some assistance, we
|
||||
would suggest that you{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser("https://discord.gg/zQBK97EE22", colors);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
<Button
|
||||
title="Open issue"
|
||||
onPress={() => {
|
||||
Linking.openURL(issueUrl.current);
|
||||
}}
|
||||
type="accent"
|
||||
width="100%"
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader
|
||||
title={issueTitle || "Report issue"}
|
||||
paragraph={
|
||||
issueTitle
|
||||
? "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
|
||||
: "Let us know if you have faced any issue/bug while using Notesnook."
|
||||
}
|
||||
}}
|
||||
>
|
||||
join our community on Discord.
|
||||
</Text>
|
||||
</Paragraph>
|
||||
/>
|
||||
|
||||
<Seperator half />
|
||||
|
||||
<TextInput
|
||||
placeholder="Title"
|
||||
onChangeText={(v) => (title.current = v)}
|
||||
defaultValue={title.current}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
marginBottom: 10,
|
||||
fontSize: SIZE.md,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
ref={bodyRef}
|
||||
multiline
|
||||
placeholder={`Tell us more about the issue you are facing.
|
||||
|
||||
For example:
|
||||
- What were you trying to do in the app?
|
||||
- What did you expect to happen?
|
||||
- Steps to reproduce the issue
|
||||
- Things you have tried etc.`}
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
onChangeText={(v) => (body.current = v)}
|
||||
onLayout={() => {
|
||||
if (initialLayout.current) return;
|
||||
initialLayout.current = true;
|
||||
if (body.current) {
|
||||
bodyRef.current?.setNativeProps({
|
||||
text: body.current,
|
||||
selection: {
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
maxHeight: 200,
|
||||
fontSize: SIZE.sm,
|
||||
marginBottom: 2.5,
|
||||
color: colors.primary.paragraph
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>{`App version: ${getVersion()} Platform: ${
|
||||
Platform.OS
|
||||
} Model: ${getBrand()}-${getModel()}-${getSystemVersion()}`}</Paragraph>
|
||||
|
||||
<Seperator />
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={loading ? null : "Submit"}
|
||||
loading={loading}
|
||||
width="100%"
|
||||
type="accent"
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
The information above will be publically available at{" "}
|
||||
<Text
|
||||
onPress={() => {
|
||||
Linking.openURL("https://github.com/streetwriters/notesnook");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
github.com/streetwriters/notesnook.
|
||||
</Text>{" "}
|
||||
If you want to ask something in general or need some assistance, we
|
||||
would suggest that you{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(
|
||||
"https://discord.gg/zQBK97EE22",
|
||||
colors
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
join our community on Discord.
|
||||
</Text>
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,10 +42,7 @@ import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { Issue } from "../github/issue";
|
||||
|
||||
export const makeError = (
|
||||
stack: string,
|
||||
component: string
|
||||
) => `Please let us know what happened. What steps we can take to reproduce the issue here.
|
||||
export const makeError = (stack: string, component: string) => `
|
||||
|
||||
_______________________________
|
||||
Stacktrace: In ${component}::${stack}`;
|
||||
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
DimensionValue,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle
|
||||
ViewStyle,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
@@ -92,6 +93,9 @@ export const Button = ({
|
||||
});
|
||||
const textColor = buttonType?.text ? buttonType.text : text;
|
||||
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const growFactor = 1 + (fontScale - 1) / 10;
|
||||
|
||||
const Component = bold ? Heading : Paragraph;
|
||||
|
||||
return (
|
||||
@@ -117,8 +121,11 @@ export const Button = ({
|
||||
customOpacity={buttonType?.opacity}
|
||||
customAlpha={buttonType?.alpha}
|
||||
style={{
|
||||
height: height,
|
||||
width: (width as DimensionValue) || undefined,
|
||||
height: typeof height === "number" ? height * growFactor : height,
|
||||
width:
|
||||
typeof width === "number"
|
||||
? width * growFactor
|
||||
: (width as DimensionValue) || undefined,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 5,
|
||||
alignSelf: "center",
|
||||
|
||||
@@ -21,11 +21,12 @@ import { VariantsWithStaticColors, useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useCallback } from "react";
|
||||
import {
|
||||
ColorValue,
|
||||
PressableStateCallbackType,
|
||||
Pressable as RNPressable,
|
||||
PressableProps as RNPressableProps,
|
||||
PressableStateCallbackType,
|
||||
View,
|
||||
ViewStyle
|
||||
ViewStyle,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import {
|
||||
RGB_Linear_Shade,
|
||||
@@ -256,6 +257,8 @@ export const Pressable = ({
|
||||
? 1
|
||||
: colorOpacity;
|
||||
const alpha = customAlpha ? customAlpha : isDark ? 0.03 : -0.03;
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const growFactor = 1 + (fontScale - 1) / 8;
|
||||
|
||||
const getStyle = useCallback(
|
||||
({ pressed }: PressableStateCallbackType): ViewStyle | ViewStyle[] => [
|
||||
@@ -276,7 +279,13 @@ export const Pressable = ({
|
||||
: borderColor || "transparent",
|
||||
borderWidth: borderWidth
|
||||
},
|
||||
style
|
||||
style,
|
||||
{
|
||||
height:
|
||||
typeof style.height === "number"
|
||||
? style.height * growFactor
|
||||
: style.height
|
||||
}
|
||||
],
|
||||
[
|
||||
alpha,
|
||||
@@ -288,7 +297,8 @@ export const Pressable = ({
|
||||
borderSelectedColor,
|
||||
borderColor,
|
||||
borderWidth,
|
||||
style
|
||||
style,
|
||||
growFactor
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"html-to-text": "9.0.5",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react-native-actions-sheet": "0.9.3",
|
||||
"react-native-actions-sheet": "0.9.6",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
|
||||
@@ -569,7 +569,7 @@ export const useEditor = (
|
||||
return;
|
||||
}
|
||||
|
||||
lastContentChangeTime.current[item.id] = item.dateEdited;
|
||||
lastContentChangeTime.current[item.id] = 0;
|
||||
currentLoadingNoteId.current = item.id;
|
||||
currentNotes.current[item.id] = item;
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ import { useDragState } from "./editor/state";
|
||||
import { verifyUser, verifyUserWithApplock } from "./functions";
|
||||
import { SettingSection } from "./types";
|
||||
import { getTimeLeft } from "./user-section";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
|
||||
type User = any;
|
||||
|
||||
@@ -333,12 +334,14 @@ export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
id: "logout",
|
||||
name: "Log out",
|
||||
description: "Clear all your data and reset the app.",
|
||||
description:
|
||||
"Logging out will clear all data stored on THIS DEVICE.",
|
||||
icon: "logout",
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: "Logout",
|
||||
paragraph: "Clear all your data and reset the app.",
|
||||
paragraph:
|
||||
"Logging out will clear all data stored on THIS DEVICE. Make sure you have synced all your changes before logging out.",
|
||||
positiveText: "Logout",
|
||||
positivePress: async () => {
|
||||
try {
|
||||
@@ -848,9 +851,18 @@ export const settingsGroups: SettingSection[] = [
|
||||
"Hide app contents when you switch to other apps. This will also disable screenshot taking in the app.",
|
||||
modifer: () => {
|
||||
const settings = SettingsService.get();
|
||||
Platform.OS === "android"
|
||||
? NotesnookModule.setSecureMode(!settings.privacyScreen)
|
||||
: enabled(true);
|
||||
if (Platform.OS === "ios") {
|
||||
enabled(!settings.privacyScreen);
|
||||
if (settings.privacyScreen) {
|
||||
ScreenGuardModule.unregister();
|
||||
} else {
|
||||
ScreenGuardModule.register({
|
||||
backgroundColor: "#000000"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
NotesnookModule.setSecureMode(!settings.privacyScreen);
|
||||
}
|
||||
|
||||
SettingsService.set({ privacyScreen: !settings.privacyScreen });
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { scale, updateSize } from "../utils/size";
|
||||
import { DatabaseLogger } from "../common/database";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
function reset() {
|
||||
const settings = get();
|
||||
if (settings.reminder !== "off" && settings.reminder !== "useroff") {
|
||||
@@ -117,12 +118,14 @@ function setPrivacyScreen(settings: SettingStore["settings"]) {
|
||||
NotesnookModule.setSecureMode(true);
|
||||
} else {
|
||||
enabled(true);
|
||||
ScreenGuardModule.register({ backgroundColor: "#000000" });
|
||||
}
|
||||
} else {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(false);
|
||||
} else {
|
||||
enabled(false);
|
||||
ScreenGuardModule.unregister();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3016
|
||||
versionCode 3018
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
- Fixed realtime sync issues
|
||||
- Fixed color popups not opening from main toolbar
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -1015,7 +1015,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2107;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1084,12 +1084,12 @@
|
||||
"${PODS_ROOT}/Headers/Public/#{s.name}/**",
|
||||
);
|
||||
INFOPLIST_FILE = Notesnook/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.10;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1120,7 +1120,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2107;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1189,12 +1189,12 @@
|
||||
"${PODS_ROOT}/Headers/Public/#{s.name}/**",
|
||||
);
|
||||
INFOPLIST_FILE = Notesnook/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.10;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1353,7 +1353,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2107;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1365,7 +1365,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.10;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1396,7 +1396,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2107;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1409,7 +1409,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.10;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1439,7 +1439,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2107;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1513,7 +1513,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.10;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1544,7 +1544,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2107;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1619,7 +1619,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.10;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -343,6 +343,9 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.9.0):
|
||||
- React-Core
|
||||
- react-native-screenguard (1.0.0):
|
||||
- React-Core
|
||||
- SDWebImage (~> 5.11.1)
|
||||
- react-native-share-extension (3.0.0):
|
||||
- React
|
||||
- react-native-sodium (1.5.4):
|
||||
@@ -548,6 +551,9 @@ PODS:
|
||||
- RNZipArchive/Core (6.0.9):
|
||||
- React-Core
|
||||
- SSZipArchive (~> 2.2)
|
||||
- SDWebImage (5.11.1):
|
||||
- SDWebImage/Core (= 5.11.1)
|
||||
- SDWebImage/Core (5.11.1)
|
||||
- SexyTooltip (1.2.5):
|
||||
- pop (~> 1.0)
|
||||
- SocketRocket (0.6.0)
|
||||
@@ -606,6 +612,7 @@ DEPENDENCIES:
|
||||
- react-native-pdf (from `../../node_modules/react-native-pdf`)
|
||||
- react-native-quick-sqlite (from `../../node_modules/react-native-quick-sqlite`)
|
||||
- react-native-safe-area-context (from `../../node_modules/react-native-safe-area-context`)
|
||||
- react-native-screenguard (from `../../node_modules/react-native-screenguard`)
|
||||
- "react-native-share-extension (from `../../node_modules/@ammarahmed/react-native-share-extension`)"
|
||||
- "react-native-sodium (from `../../node_modules/@ammarahmed/react-native-sodium`)"
|
||||
- react-native-theme-switch-animation (from `../../node_modules/react-native-theme-switch-animation`)
|
||||
@@ -664,6 +671,7 @@ SPEC REPOS:
|
||||
- MMKV
|
||||
- MMKVCore
|
||||
- pop
|
||||
- SDWebImage
|
||||
- SocketRocket
|
||||
- SSZipArchive
|
||||
- SwiftyRSA
|
||||
@@ -754,6 +762,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/react-native-quick-sqlite"
|
||||
react-native-safe-area-context:
|
||||
:path: "../../node_modules/react-native-safe-area-context"
|
||||
react-native-screenguard:
|
||||
:path: "../../node_modules/react-native-screenguard"
|
||||
react-native-share-extension:
|
||||
:path: "../../node_modules/@ammarahmed/react-native-share-extension"
|
||||
react-native-sodium:
|
||||
@@ -905,6 +915,7 @@ SPEC CHECKSUMS:
|
||||
react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
|
||||
react-native-quick-sqlite: e0e23b749382a85e4b57146f753de737a6c3a9e1
|
||||
react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b
|
||||
react-native-screenguard: 8b36a3df84c76cd2b82c477f71c26fa1c8cc14a0
|
||||
react-native-share-extension: faed334b1ddf165f1e576fcabd3dc1c9e748bfa9
|
||||
react-native-sodium: 955bb0dc3ea05f8ea06d5e96cb89d1be7b5d7681
|
||||
react-native-theme-switch-animation: 220f883f7be290e79f2ab022093ed1a7a5929e6d
|
||||
@@ -949,6 +960,7 @@ SPEC CHECKSUMS:
|
||||
RNSVG: d7d7bc8229af3842c9cfc3a723c815a52cdd1105
|
||||
RNTooltips: 5424d4bf0b3d441104127943b1115cc7f0616b1f
|
||||
RNZipArchive: 68a0c6db4b1c103f846f1559622050df254a3ade
|
||||
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||
SexyTooltip: 5c9b4dec52bfb317938cb0488efd9da3717bb6fd
|
||||
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
@@ -959,4 +971,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.14.2
|
||||
|
||||
@@ -66,7 +66,8 @@
|
||||
"react-native-theme-switch-animation": "^0.6.0",
|
||||
"@ammarahmed/react-native-background-fetch": "^4.2.2",
|
||||
"react-native-image-crop-picker": "^0.40.2",
|
||||
"react-native-url-polyfill": "^2.0.0"
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-screenguard": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
|
||||
@@ -17,6 +17,12 @@ config.dependencies['react-native-vector-icons'] = {
|
||||
},
|
||||
}
|
||||
|
||||
config.dependencies['react-native-screenguard'] = {
|
||||
platforms: {
|
||||
android: null,
|
||||
},
|
||||
}
|
||||
|
||||
if (isGithubRelease) {
|
||||
config.dependencies["react-native-iap"] = {
|
||||
platforms: {
|
||||
|
||||
44
apps/mobile/package-lock.json
generated
44
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.9",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -34,15 +34,15 @@
|
||||
},
|
||||
"../../packages/common": {
|
||||
"name": "@notesnook/common",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/core": "file:../core",
|
||||
"@notesnook/core": "^8.0.0",
|
||||
"pathe": "^1.1.2",
|
||||
"timeago.js": "4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@notesnook/core": "file:../core",
|
||||
"@notesnook/core": "^8.0.0",
|
||||
"@types/react": "^18.2.39",
|
||||
"react": "18.2.0",
|
||||
"vitest": "^1.4.0"
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"../../packages/core": {
|
||||
"name": "@notesnook/core",
|
||||
"version": "7.4.1",
|
||||
"version": "8.0.2",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
@@ -3121,7 +3121,7 @@
|
||||
},
|
||||
"../../packages/crypto": {
|
||||
"name": "@notesnook/crypto",
|
||||
"version": "1.1.1",
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -3135,7 +3135,7 @@
|
||||
},
|
||||
"../../packages/editor": {
|
||||
"name": "@notesnook/editor",
|
||||
"version": "1.6.1",
|
||||
"version": "2.0.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -23795,13 +23795,13 @@
|
||||
},
|
||||
"../../packages/logger": {
|
||||
"name": "@notesnook/logger",
|
||||
"version": "1.0.3",
|
||||
"version": "2.0.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {}
|
||||
},
|
||||
"../../packages/sodium": {
|
||||
"name": "@notesnook/sodium",
|
||||
"version": "1.1.0",
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
@@ -25173,7 +25173,7 @@
|
||||
},
|
||||
"../../packages/theme": {
|
||||
"name": "@notesnook/theme",
|
||||
"version": "1.2.0",
|
||||
"version": "2.0.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
@@ -26240,7 +26240,7 @@
|
||||
},
|
||||
"../../packages/ui": {
|
||||
"name": "@notesnook/ui",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.2",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/theme": "file:../theme"
|
||||
@@ -28412,7 +28412,7 @@
|
||||
"pathe": "1.1.2",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react-native-actions-sheet": "0.9.3",
|
||||
"react-native-actions-sheet": "0.9.6",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
@@ -28487,6 +28487,7 @@
|
||||
"react-native-reanimated": "3.3.0",
|
||||
"react-native-safe-area-context": "^4.3.1",
|
||||
"react-native-scoped-storage": "^1.9.5",
|
||||
"react-native-screenguard": "^1.0.0",
|
||||
"react-native-screens": "^3.13.1",
|
||||
"react-native-securerandom": "^1.0.1",
|
||||
"react-native-share": "^7.2.0",
|
||||
@@ -44778,8 +44779,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-actions-sheet": {
|
||||
"version": "0.9.3",
|
||||
"license": "MIT",
|
||||
"version": "0.9.6",
|
||||
"resolved": "https://registry.npmjs.org/react-native-actions-sheet/-/react-native-actions-sheet-0.9.6.tgz",
|
||||
"integrity": "sha512-BMEFmJD29izbOxkH81HJWNCgrjdW5iz4gB+Eyov9xwUPz/Fbi53R6AFvF9N3ttYT8nEvzxj56nqP7uJZ6KuKuA==",
|
||||
"dependencies": {
|
||||
"react-native-safe-area-context": "^4.8.2"
|
||||
},
|
||||
@@ -45253,6 +45255,18 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-screenguard": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-screenguard/-/react-native-screenguard-1.0.0.tgz",
|
||||
"integrity": "sha512-fqtoJI8TxszKqrntnHW48EMb4gNVA9VCAg5UG1FcIXEyv2W70kWoMxRRFoWuCRYlBWSAWqiXO+R6Xfk/0um0cw==",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-screens": {
|
||||
"version": "3.21.1",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.8",
|
||||
"version": "3.0.10",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
54
apps/mobile/patches/react-native-screenguard+1.0.0.patch
Normal file
54
apps/mobile/patches/react-native-screenguard+1.0.0.patch
Normal file
@@ -0,0 +1,54 @@
|
||||
diff --git a/node_modules/react-native-screenguard/ios/ScreenGuard.mm b/node_modules/react-native-screenguard/ios/ScreenGuard.mm
|
||||
index 9fa7d33..d2e6d68 100644
|
||||
--- a/node_modules/react-native-screenguard/ios/ScreenGuard.mm
|
||||
+++ b/node_modules/react-native-screenguard/ios/ScreenGuard.mm
|
||||
@@ -11,7 +11,7 @@
|
||||
@implementation ScreenGuard
|
||||
RCT_EXPORT_MODULE(ScreenGuard)
|
||||
|
||||
-bool hasListeners;
|
||||
+bool hasListeners_;
|
||||
|
||||
UITextField *textField;
|
||||
UIImageView *imageView;
|
||||
@@ -22,11 +22,11 @@ @implementation ScreenGuard
|
||||
}
|
||||
|
||||
- (void)startObserving {
|
||||
- hasListeners = YES;
|
||||
+ hasListeners_ = YES;
|
||||
}
|
||||
|
||||
- (void)stopObserving {
|
||||
- hasListeners = NO;
|
||||
+ hasListeners_ = NO;
|
||||
}
|
||||
|
||||
- (void)secureViewWithBackgroundColor: (NSString *)color {
|
||||
@@ -335,7 +335,7 @@ - (UIImage *)convertViewToImage:(UIView *)view {
|
||||
queue:mainQueue
|
||||
usingBlock:^(NSNotification *notification) {
|
||||
|
||||
- if (hasListeners && getScreenShotPath) {
|
||||
+ if (hasListeners_ && getScreenShotPath) {
|
||||
UIViewController *presentedViewController = RCTPresentedViewController();
|
||||
|
||||
UIImage *image = [self convertViewToImage:presentedViewController.view.superview];
|
||||
@@ -359,7 +359,7 @@ - (UIImage *)convertViewToImage:(UIView *)view {
|
||||
result = @{@"path": filePath, @"name": fileName, @"type": @"PNG"};
|
||||
}
|
||||
[self emit:SCREENSHOT_EVT body: result];
|
||||
- } else if (hasListeners) {
|
||||
+ } else if (hasListeners_) {
|
||||
[self emit:SCREENSHOT_EVT body: nil];
|
||||
}
|
||||
}];
|
||||
@@ -376,7 +376,7 @@ - (UIImage *)convertViewToImage:(UIView *)view {
|
||||
queue:mainQueue
|
||||
usingBlock:^(NSNotification *notification) {
|
||||
|
||||
- if (hasListeners) {
|
||||
+ if (hasListeners_) {
|
||||
[self emit:SCREEN_RECORDING_EVT body:nil];
|
||||
}
|
||||
}];
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=0">
|
||||
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=1690887574068">
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=0">
|
||||
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=1690887574068">
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=0">
|
||||
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=1690887574068">
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ test("logged in user should not be able to open unauthorized routes", async ({
|
||||
await page.goto(route);
|
||||
|
||||
await page.waitForURL(/\/notes/gm);
|
||||
await page.waitForTimeout(1000);
|
||||
expect(await app.navigation.findItem("Notes")).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
4990
apps/web/package-lock.json
generated
4990
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.9",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -89,7 +89,8 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@swc/core": "1.3.61",
|
||||
"@swc/core": "^1.5.24",
|
||||
"@swc/plugin-react-remove-properties": "^2.0.4",
|
||||
"@trpc/server": "10.38.3",
|
||||
"@types/babel__core": "^7.20.1",
|
||||
"@types/event-source-polyfill": "^1.0.1",
|
||||
@@ -104,8 +105,8 @@
|
||||
"@types/react-scroll-sync": "^0.9.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/wicg-file-system-access": "^2020.9.6",
|
||||
"@vitejs/plugin-react-swc": "3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"better-sqlite3-multiple-ciphers": "^9.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"chalk": "^4.1.0",
|
||||
@@ -116,14 +117,13 @@
|
||||
"ip": "^1.1.8",
|
||||
"lorem-ipsum": "^2.0.4",
|
||||
"otplib": "^12.0.1",
|
||||
"rollup": "^3.29.4",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"swc-plugin-react-remove-properties": "^0.1.4",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-env-compatible": "^1.1.1",
|
||||
"vite-plugin-pwa": "^0.16.3",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"vitest": "^0.34.6",
|
||||
"rollup": "^4.18.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"vite": "^5.2.12",
|
||||
"vite-plugin-env-compatible": "^2.0.1",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vitest": "^1.6.0",
|
||||
"workbox-core": "^7.0.0",
|
||||
"workbox-expiration": "^7.0.0",
|
||||
"workbox-precaching": "^7.0.0",
|
||||
|
||||
@@ -58,9 +58,8 @@ export default function MobileAppEffects({
|
||||
overlay.style.pointerEvents = "none";
|
||||
}
|
||||
},
|
||||
onChange: (e, { slide, lastSlide }) => {
|
||||
if (!lastSlide || !isMobile) return;
|
||||
toggleSideMenu(slide?.index === 1 ? true : false);
|
||||
onChange: (e, { slide }) => {
|
||||
toggleSideMenu(slide?.index === 0 ? true : false);
|
||||
setIsEditorOpen(slide?.index === 3 ? true : false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,10 +22,8 @@ import { Box, Flex } from "@theme-ui/components";
|
||||
import { ScopedThemeProvider } from "./components/theme-provider";
|
||||
import useMobile from "./hooks/use-mobile";
|
||||
import useTablet from "./hooks/use-tablet";
|
||||
import useDatabase from "./hooks/use-database";
|
||||
import { useStore } from "./stores/app-store";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { ViewLoader } from "./components/loaders/view-loader";
|
||||
import NavigationMenu from "./components/navigation-menu";
|
||||
import StatusBar from "./components/status-bar";
|
||||
import { EditorLoader } from "./components/loaders/editor-loader";
|
||||
@@ -38,12 +36,13 @@ import {
|
||||
PanelResizeHandle,
|
||||
ImperativePanelHandle
|
||||
} from "react-resizable-panels";
|
||||
import GlobalMenuWrapper from "./components/global-menu-wrapper";
|
||||
|
||||
new WebExtensionRelay();
|
||||
|
||||
const GlobalMenuWrapper = React.lazy(
|
||||
() => import("./components/global-menu-wrapper")
|
||||
);
|
||||
// const GlobalMenuWrapper = React.lazy(
|
||||
// () => import("./components/global-menu-wrapper")
|
||||
// );
|
||||
const AppEffects = React.lazy(() => import("./app-effects"));
|
||||
const MobileAppEffects = React.lazy(() => import("./app-effects.mobile"));
|
||||
const HashRouter = React.lazy(() => import("./components/hash-router"));
|
||||
@@ -51,26 +50,24 @@ const HashRouter = React.lazy(() => import("./components/hash-router"));
|
||||
function App() {
|
||||
const isMobile = useMobile();
|
||||
const [show, setShow] = useState(true);
|
||||
const [isAppLoaded] = useDatabase();
|
||||
const isFocusMode = useStore((store) => store.isFocusMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAppLoaded && (
|
||||
<Suspense fallback={<div style={{ display: "none" }} />}>
|
||||
<div id="menu-wrapper">
|
||||
<GlobalMenuWrapper />
|
||||
</div>
|
||||
<AppEffects setShow={setShow} />
|
||||
{isMobile && (
|
||||
<MobileAppEffects
|
||||
sliderId="slider"
|
||||
overlayId="overlay"
|
||||
setShow={setShow}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
)}
|
||||
<Suspense fallback={<div style={{ display: "none" }} />}>
|
||||
<div id="menu-wrapper">
|
||||
<GlobalMenuWrapper />
|
||||
</div>
|
||||
<AppEffects setShow={setShow} />
|
||||
{isMobile && (
|
||||
<MobileAppEffects
|
||||
sliderId="slider"
|
||||
overlayId="overlay"
|
||||
setShow={setShow}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
|
||||
<Flex
|
||||
id="app"
|
||||
bg="background"
|
||||
@@ -78,13 +75,9 @@ function App() {
|
||||
sx={{ overflow: "hidden", flexDirection: "column", height: "100%" }}
|
||||
>
|
||||
{isMobile ? (
|
||||
<MobileAppContents isAppLoaded={isAppLoaded} />
|
||||
<MobileAppContents />
|
||||
) : (
|
||||
<DesktopAppContents
|
||||
isAppLoaded={isAppLoaded}
|
||||
setShow={setShow}
|
||||
show={show}
|
||||
/>
|
||||
<DesktopAppContents setShow={setShow} show={show} />
|
||||
)}
|
||||
<Toaster containerClassName="toasts-container" />
|
||||
</Flex>
|
||||
@@ -121,15 +114,10 @@ function SuspenseLoader<TComponent extends React.JSXElementConstructor<any>>({
|
||||
}
|
||||
|
||||
type DesktopAppContentsProps = {
|
||||
isAppLoaded: boolean;
|
||||
show: boolean;
|
||||
setShow: (show: boolean) => void;
|
||||
};
|
||||
function DesktopAppContents({
|
||||
isAppLoaded,
|
||||
show,
|
||||
setShow
|
||||
}: DesktopAppContentsProps) {
|
||||
function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
|
||||
const isFocusMode = useStore((store) => store.isFocusMode);
|
||||
const isTablet = useTablet();
|
||||
const [isNarrow, setIsNarrow] = useState(isTablet || false);
|
||||
@@ -137,19 +125,23 @@ function DesktopAppContents({
|
||||
const middlePane = useRef<ImperativePanelHandle>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (show) middlePane.current?.expand();
|
||||
else middlePane.current?.collapse();
|
||||
}, [show]);
|
||||
setIsNarrow(isTablet);
|
||||
}, [isTablet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocusMode) {
|
||||
const middlePaneSize = middlePane.current?.getSize() || 20;
|
||||
navPane.current?.collapse();
|
||||
// the middle pane has to be resized because collapsing the nav
|
||||
// pane increases the middle pane's size every time.
|
||||
middlePane.current?.resize(middlePaneSize);
|
||||
} else navPane.current?.expand();
|
||||
}, [isFocusMode]);
|
||||
// useEffect(() => {
|
||||
// if (show) middlePane.current?.expand();
|
||||
// else middlePane.current?.collapse();
|
||||
// }, [show]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (isFocusMode) {
|
||||
// const middlePaneSize = middlePane.current?.getSize() || 20;
|
||||
// navPane.current?.collapse();
|
||||
// // the middle pane has to be resized because collapsing the nav
|
||||
// // pane increases the middle pane's size every time.
|
||||
// middlePane.current?.resize(middlePaneSize);
|
||||
// } else navPane.current?.expand();
|
||||
// }, [isFocusMode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -160,45 +152,67 @@ function DesktopAppContents({
|
||||
}}
|
||||
>
|
||||
<PanelGroup autoSaveId="global-panel-group" direction="horizontal">
|
||||
<Panel
|
||||
ref={navPane}
|
||||
className="nav-pane"
|
||||
defaultSize={10}
|
||||
minSize={3.5}
|
||||
onResize={(size) => setIsNarrow(size <= 5)}
|
||||
collapsible
|
||||
collapsedSize={3.5}
|
||||
>
|
||||
<NavigationMenu
|
||||
toggleNavigationContainer={(state) => {
|
||||
setShow(state || !show);
|
||||
}}
|
||||
isTablet={isNarrow}
|
||||
/>
|
||||
</Panel>
|
||||
<PanelResizeHandle className="panel-resize-handle" />
|
||||
<Panel
|
||||
ref={middlePane}
|
||||
className="middle-pane"
|
||||
collapsible
|
||||
defaultSize={20}
|
||||
>
|
||||
<ScopedThemeProvider
|
||||
className="listMenu"
|
||||
scope="list"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
bg: "background",
|
||||
borderRight: "1px solid var(--separator)"
|
||||
}}
|
||||
>
|
||||
{isAppLoaded && <CachedRouter />}
|
||||
</ScopedThemeProvider>
|
||||
</Panel>
|
||||
<PanelResizeHandle className="panel-resize-handle" />
|
||||
<Panel className="editor-pane" defaultSize={70}>
|
||||
{!isFocusMode && isTablet ? (
|
||||
<Flex sx={{ width: 50 }}>
|
||||
<NavigationMenu
|
||||
toggleNavigationContainer={(state) => {
|
||||
setShow(state || !show);
|
||||
}}
|
||||
isTablet={isNarrow}
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
!isFocusMode && (
|
||||
<>
|
||||
<Panel
|
||||
ref={navPane}
|
||||
order={1}
|
||||
className="nav-pane"
|
||||
defaultSize={10}
|
||||
minSize={3.5}
|
||||
// maxSize={isNarrow ? 5 : undefined}
|
||||
onResize={(size) => setIsNarrow(size <= 5)}
|
||||
collapsible
|
||||
collapsedSize={3.5}
|
||||
>
|
||||
<NavigationMenu
|
||||
toggleNavigationContainer={(state) => {
|
||||
setShow(state || !show);
|
||||
}}
|
||||
isTablet={isNarrow}
|
||||
/>
|
||||
</Panel>
|
||||
<PanelResizeHandle className="panel-resize-handle" />
|
||||
</>
|
||||
)
|
||||
)}
|
||||
{!isFocusMode && show && (
|
||||
<>
|
||||
<Panel
|
||||
ref={middlePane}
|
||||
className="middle-pane"
|
||||
order={2}
|
||||
collapsible
|
||||
defaultSize={20}
|
||||
>
|
||||
<ScopedThemeProvider
|
||||
className="listMenu"
|
||||
scope="list"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
bg: "background",
|
||||
borderRight: "1px solid var(--separator)"
|
||||
}}
|
||||
>
|
||||
<CachedRouter />
|
||||
</ScopedThemeProvider>
|
||||
</Panel>
|
||||
<PanelResizeHandle className="panel-resize-handle" />
|
||||
</>
|
||||
)}
|
||||
<Panel className="editor-pane" order={3} defaultSize={70}>
|
||||
<Flex
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -208,7 +222,7 @@ function DesktopAppContents({
|
||||
bg: "background"
|
||||
}}
|
||||
>
|
||||
{isAppLoaded && <HashRouter />}
|
||||
{<HashRouter />}
|
||||
</Flex>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
@@ -218,7 +232,7 @@ function DesktopAppContents({
|
||||
);
|
||||
}
|
||||
|
||||
function MobileAppContents({ isAppLoaded }: { isAppLoaded: boolean }) {
|
||||
function MobileAppContents() {
|
||||
return (
|
||||
<FlexScrollContainer
|
||||
id="slider"
|
||||
@@ -257,11 +271,7 @@ function MobileAppContents({ isAppLoaded }: { isAppLoaded: boolean }) {
|
||||
width: "100vw"
|
||||
}}
|
||||
>
|
||||
<SuspenseLoader
|
||||
condition={isAppLoaded}
|
||||
component={CachedRouter}
|
||||
fallback={<ViewLoader />}
|
||||
/>
|
||||
<CachedRouter />
|
||||
<Box
|
||||
id="overlay"
|
||||
sx={{
|
||||
@@ -290,7 +300,7 @@ function MobileAppContents({ isAppLoaded }: { isAppLoaded: boolean }) {
|
||||
<SuspenseLoader
|
||||
fallback={<EditorLoader />}
|
||||
component={HashRouter}
|
||||
condition={isAppLoaded}
|
||||
condition={true}
|
||||
/>
|
||||
</Flex>
|
||||
</FlexScrollContainer>
|
||||
|
||||
@@ -18,11 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./polyfills";
|
||||
import "@notesnook/core/dist/types";
|
||||
import { getCurrentHash, getCurrentPath, makeURL } from "./navigation";
|
||||
import Config from "./utils/config";
|
||||
|
||||
import { initializeLogger, logger } from "./utils/logger";
|
||||
// import { initializeLogger, logger } from "./utils/logger";
|
||||
import type { AuthProps } from "./views/auth";
|
||||
import { initializeFeatureChecks } from "./utils/feature-check";
|
||||
|
||||
@@ -102,7 +101,7 @@ const sessionExpiryExceptions: Routes[] = [
|
||||
|
||||
function getRoute(): RouteWithPath<AuthProps> | RouteWithPath {
|
||||
const path = getCurrentPath() as Routes;
|
||||
logger.info(`Getting route for path: ${path}`);
|
||||
// logger.info(`Getting route for path: ${path}`);
|
||||
|
||||
const signup = redirectToRegistration(path);
|
||||
const sessionExpired = isSessionExpired(path);
|
||||
@@ -129,7 +128,7 @@ function redirectToRegistration(path: Routes): RouteWithPath<AuthProps> | null {
|
||||
function isSessionExpired(path: Routes): RouteWithPath<AuthProps> | null {
|
||||
const isSessionExpired = Config.get("sessionExpired", false);
|
||||
if (isSessionExpired && !sessionExpiryExceptions.includes(path)) {
|
||||
logger.info(`User session has expired. Routing to /sessionexpired`);
|
||||
// logger.info(`User session has expired. Routing to /sessionexpired`);
|
||||
|
||||
window.history.replaceState(
|
||||
{},
|
||||
@@ -143,7 +142,10 @@ function isSessionExpired(path: Routes): RouteWithPath<AuthProps> | null {
|
||||
|
||||
export async function init() {
|
||||
await initializeFeatureChecks();
|
||||
await initializeLogger();
|
||||
|
||||
await import("./utils/logger").then(({ initializeLogger }) =>
|
||||
initializeLogger()
|
||||
);
|
||||
|
||||
const { path, route } = getRoute();
|
||||
return { ...route, path };
|
||||
|
||||
@@ -25,7 +25,7 @@ import { database } from "@notesnook/common";
|
||||
import { createDialect } from "./sqlite";
|
||||
import { isFeatureSupported } from "../utils/feature-check";
|
||||
import { generatePassword } from "../utils/password-generator";
|
||||
import { deriveKey } from "../interfaces/key-store";
|
||||
import { deriveKey, useKeyStore } from "../interfaces/key-store";
|
||||
import { logManager } from "@notesnook/core/dist/logger";
|
||||
|
||||
const db = database;
|
||||
@@ -34,7 +34,6 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
|
||||
const { FileStorage } = await import("../interfaces/fs");
|
||||
const { Compressor } = await import("../utils/compressor");
|
||||
const { useKeyStore } = await import("../interfaces/key-store");
|
||||
|
||||
let databaseKey = await useKeyStore.getState().getValue("databaseKey");
|
||||
if (!databaseKey) {
|
||||
@@ -59,7 +58,8 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
|
||||
database.setup({
|
||||
sqliteOptions: {
|
||||
dialect: (name, init) => createDialect(name, true, init),
|
||||
dialect: (name, init) =>
|
||||
createDialect(persistence === "memory" ? ":memory:" : name, true, init),
|
||||
...(IS_DESKTOP_APP || isFeatureSupported("opfs")
|
||||
? { journalMode: "WAL", lockingMode: "exclusive" }
|
||||
: {
|
||||
@@ -71,7 +71,7 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
pageSize: 8192,
|
||||
cacheSize: -32000,
|
||||
password: Buffer.from(databaseKey).toString("hex"),
|
||||
skipInitialization: !IS_DESKTOP_APP
|
||||
skipInitialization: !IS_DESKTOP_APP && !!globalThis.SharedWorker
|
||||
},
|
||||
storage: storage,
|
||||
eventsource: EventSource,
|
||||
@@ -98,7 +98,9 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
// });
|
||||
// }
|
||||
|
||||
console.log("loading db");
|
||||
await db.init();
|
||||
console.log("db loaded");
|
||||
|
||||
window.addEventListener("beforeunload", async () => {
|
||||
if (IS_DESKTOP_APP) {
|
||||
|
||||
@@ -213,7 +213,7 @@ export function showLogoutConfirmation() {
|
||||
return confirm({
|
||||
title: `Logout?`,
|
||||
message:
|
||||
"Logging out will delete all local data and reset the app. Make sure you have synced your data before logging out.",
|
||||
"Logging out will clear all data stored on THIS DEVICE. Make sure you have synced all your changes before logging out.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
|
||||
@@ -100,17 +100,26 @@ export async function exportNotes(
|
||||
};
|
||||
}
|
||||
});
|
||||
confirm({
|
||||
title: `Exported ${result.count} notes`,
|
||||
message:
|
||||
result.errors.length > 0
|
||||
? `Export completed with ${result.errors.length} errors:
|
||||
if (result instanceof Error) {
|
||||
confirm({
|
||||
title: `Export failed`,
|
||||
message: result.stack || result.message,
|
||||
positiveButtonText: "Okay"
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
confirm({
|
||||
title: `Exported ${result.count} notes`,
|
||||
message:
|
||||
result.errors.length > 0
|
||||
? `Export completed with ${result.errors.length} errors:
|
||||
|
||||
${result.errors.map((e, i) => `${i + 1}. ${e.message}`).join("\n")}`
|
||||
: "Export completed with 0 errors.",
|
||||
positiveButtonText: "Okay"
|
||||
});
|
||||
return true;
|
||||
: "Export completed with 0 errors.",
|
||||
positiveButtonText: "Okay"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const FORMAT_TO_EXT = {
|
||||
|
||||
@@ -23,7 +23,10 @@ import {
|
||||
SqliteIntrospector,
|
||||
Dialect
|
||||
} from "kysely";
|
||||
import { WaSqliteWorkerDriver } from "./wa-sqlite-kysely-driver";
|
||||
import {
|
||||
WaSqliteWorkerMultipleTabDriver,
|
||||
WaSqliteWorkerSingleTabDriver
|
||||
} from "./wa-sqlite-kysely-driver";
|
||||
import { isFeatureSupported } from "../../utils/feature-check";
|
||||
|
||||
declare module "kysely" {
|
||||
@@ -39,12 +42,18 @@ export const createDialect = (
|
||||
): Dialect => {
|
||||
return {
|
||||
createDriver: () =>
|
||||
new WaSqliteWorkerDriver({
|
||||
async: !isFeatureSupported("opfs"),
|
||||
dbName: name,
|
||||
encrypted,
|
||||
init
|
||||
}),
|
||||
globalThis.SharedWorker
|
||||
? new WaSqliteWorkerMultipleTabDriver({
|
||||
async: !isFeatureSupported("opfs"),
|
||||
dbName: name,
|
||||
encrypted,
|
||||
init
|
||||
})
|
||||
: new WaSqliteWorkerSingleTabDriver({
|
||||
async: !isFeatureSupported("opfs"),
|
||||
dbName: name,
|
||||
encrypted
|
||||
}),
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
|
||||
@@ -182,7 +182,9 @@ export class SharedService<T extends object> extends EventTarget {
|
||||
}
|
||||
|
||||
#sendPortToClient(message: any, port: MessagePort) {
|
||||
sharedWorker?.port.postMessage(message, [port]);
|
||||
if (!sharedWorker)
|
||||
throw new Error("Shared worker is not supported in this environment.");
|
||||
sharedWorker.port.postMessage(message, [port]);
|
||||
}
|
||||
|
||||
async #getClientId() {
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import type { SQLiteAPI, SQLiteCompatibleType } from "./sqlite-types";
|
||||
import { Factory, SQLITE_ROW, SQLiteError } from "./sqlite-api";
|
||||
import { transfer } from "comlink";
|
||||
import { expose, transfer } from "comlink";
|
||||
import type { RunMode } from "./type";
|
||||
import { QueryResult } from "kysely";
|
||||
import { DatabaseSource } from "./sqlite-export";
|
||||
@@ -32,6 +32,12 @@ type PreparedStatement = {
|
||||
columns: string[];
|
||||
};
|
||||
|
||||
type SQLiteOptions = {
|
||||
async: boolean;
|
||||
url?: string;
|
||||
encrypted: boolean;
|
||||
};
|
||||
|
||||
class _SQLiteWorker {
|
||||
sqlite!: SQLiteAPI;
|
||||
db: number | undefined = undefined;
|
||||
@@ -39,21 +45,22 @@ class _SQLiteWorker {
|
||||
initialized = false;
|
||||
preparedStatements: Map<string, PreparedStatement> = new Map();
|
||||
retryCounter: Record<string, number> = {};
|
||||
constructor(
|
||||
private readonly dbName: string,
|
||||
private readonly encrypted: boolean
|
||||
) {
|
||||
console.log("new sqlite worker", dbName, encrypted);
|
||||
}
|
||||
encrypted = false;
|
||||
name = "";
|
||||
async = false;
|
||||
|
||||
async open(async: boolean, url?: string) {
|
||||
async open(name: string, options: SQLiteOptions) {
|
||||
if (this.db) {
|
||||
console.error("Database is already initialized", this.db);
|
||||
return;
|
||||
}
|
||||
|
||||
const option = url ? { locateFile: () => url } : {};
|
||||
const sqliteModule = async
|
||||
this.encrypted = options.encrypted;
|
||||
this.name = name;
|
||||
this.async = options.async;
|
||||
|
||||
const option = options.url ? { locateFile: () => options.url } : {};
|
||||
const sqliteModule = options.async
|
||||
? await import("./wa-sqlite-async").then(
|
||||
({ default: SQLiteAsyncESMFactory }) => SQLiteAsyncESMFactory(option)
|
||||
)
|
||||
@@ -61,11 +68,11 @@ class _SQLiteWorker {
|
||||
SQLiteSyncESMFactory(option)
|
||||
);
|
||||
this.sqlite = Factory(sqliteModule);
|
||||
this.vfs = await this.getVFS(this.dbName, async);
|
||||
this.vfs = await this.getVFS(name, options.async);
|
||||
|
||||
this.sqlite.vfs_register(this.vfs, false);
|
||||
this.db = await this.sqlite.open_v2(
|
||||
this.dbName,
|
||||
name,
|
||||
undefined,
|
||||
`multipleciphers-${this.vfs.name}`
|
||||
);
|
||||
@@ -163,7 +170,7 @@ class _SQLiteWorker {
|
||||
if (this.encrypted && !sql.startsWith("PRAGMA key")) {
|
||||
await this.waitForDatabase();
|
||||
}
|
||||
if (!this.db) throw new Error("No database is not opened.");
|
||||
if (!this.db) throw new Error("Database is not opened.");
|
||||
|
||||
const rows = (await this.exec(sql, mode, parameters)) as R[];
|
||||
if (mode === "query") return { rows };
|
||||
@@ -194,16 +201,16 @@ class _SQLiteWorker {
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async export(dbName: string, async: boolean) {
|
||||
const vfs = await this.getVFS(dbName, async);
|
||||
const stream = new ReadableStream(new DatabaseSource(vfs, dbName));
|
||||
async export() {
|
||||
const vfs = await this.getVFS(this.name, this.async);
|
||||
const stream = new ReadableStream(new DatabaseSource(vfs, this.name));
|
||||
return transfer(stream, [stream]);
|
||||
}
|
||||
|
||||
async delete(dbName: string, async: boolean) {
|
||||
async delete() {
|
||||
await this.close();
|
||||
if (this.vfs) await this.vfs.delete();
|
||||
else await (await this.getVFS(dbName, async)).delete();
|
||||
else await (await this.getVFS(this.name, this.async)).delete();
|
||||
}
|
||||
|
||||
async getVFS(dbName: string, async: boolean) {
|
||||
@@ -222,7 +229,7 @@ class _SQLiteWorker {
|
||||
async initialize() {
|
||||
self.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: { type: "databaseInitialized", dbName: this.dbName }
|
||||
data: { type: "databaseInitialized", dbName: this.name }
|
||||
})
|
||||
);
|
||||
console.log("Database initialized", this.db);
|
||||
@@ -237,7 +244,7 @@ class _SQLiteWorker {
|
||||
self.addEventListener("message", (ev) => {
|
||||
if (
|
||||
ev.data.type === "databaseInitialized" &&
|
||||
ev.data.dbName === this.dbName
|
||||
ev.data.dbName === this.name
|
||||
)
|
||||
resolve(true);
|
||||
})
|
||||
@@ -251,11 +258,17 @@ export type SQLiteWorker = typeof _SQLiteWorker.prototype;
|
||||
|
||||
addEventListener("message", async (event) => {
|
||||
if (!event.data.type) {
|
||||
const worker = new _SQLiteWorker(event.data.dbName, event.data.encrypted);
|
||||
await worker.open(event.data.async, event.data.uri);
|
||||
const worker = new _SQLiteWorker();
|
||||
await worker.open(event.data.dbName, {
|
||||
async: event.data.async,
|
||||
encrypted: event.data.encrypted,
|
||||
url: event.data.uri
|
||||
});
|
||||
const providerPort = createSharedServicePort(worker);
|
||||
postMessage(null, [providerPort]);
|
||||
|
||||
self.addEventListener("beforeunload", () => worker.close());
|
||||
}
|
||||
});
|
||||
const worker = new _SQLiteWorker();
|
||||
expose(worker);
|
||||
|
||||
@@ -25,6 +25,7 @@ import SQLiteSyncURI from "./wa-sqlite.wasm?url";
|
||||
import SQLiteAsyncURI from "./wa-sqlite-async.wasm?url";
|
||||
import { Mutex } from "async-mutex";
|
||||
import { SharedService } from "./shared-service";
|
||||
import { Remote, wrap } from "comlink";
|
||||
|
||||
type Config = {
|
||||
dbName: string;
|
||||
@@ -38,13 +39,14 @@ const servicePool = new Map<
|
||||
{ service: SharedService<SQLiteWorker>; activated: boolean; closed: boolean }
|
||||
>();
|
||||
|
||||
export class WaSqliteWorkerDriver implements Driver {
|
||||
export class WaSqliteWorkerMultipleTabDriver implements Driver {
|
||||
private connection?: DatabaseConnection;
|
||||
private connectionMutex = new Mutex();
|
||||
private initializationMutex = new Mutex();
|
||||
private readonly serviceName;
|
||||
|
||||
constructor(private readonly config: Config) {
|
||||
console.log("multi tab driver", config.dbName);
|
||||
this.serviceName = `${config.dbName}-service`;
|
||||
}
|
||||
|
||||
@@ -59,10 +61,11 @@ export class WaSqliteWorkerDriver implements Driver {
|
||||
if (activated) {
|
||||
if (closed) {
|
||||
console.log("Already activated. Reinitializing...");
|
||||
await service.proxy.open(
|
||||
this.config.async,
|
||||
this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
||||
);
|
||||
await service.proxy.open(this.config.dbName, {
|
||||
async: this.config.async,
|
||||
encrypted: this.config.encrypted,
|
||||
url: this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
||||
});
|
||||
this.needsInitialization = true;
|
||||
servicePool.set(this.serviceName, {
|
||||
service,
|
||||
@@ -193,19 +196,76 @@ export class WaSqliteWorkerDriver implements Driver {
|
||||
async delete() {
|
||||
const service = servicePool.get(this.serviceName);
|
||||
if (!service || !service.service) return;
|
||||
await service.service?.proxy?.delete(this.config.dbName, this.config.async);
|
||||
await service.service?.proxy?.delete();
|
||||
service.closed = true;
|
||||
}
|
||||
|
||||
async export() {
|
||||
return servicePool
|
||||
.get(this.serviceName)
|
||||
?.service?.proxy?.export(this.config.dbName, this.config.async);
|
||||
return servicePool.get(this.serviceName)?.service?.proxy?.export();
|
||||
}
|
||||
}
|
||||
|
||||
export class WaSqliteWorkerSingleTabDriver implements Driver {
|
||||
private connection?: DatabaseConnection;
|
||||
private connectionMutex = new Mutex();
|
||||
private readonly worker = wrap<SQLiteWorker>(
|
||||
new Worker({ name: this.config.dbName })
|
||||
);
|
||||
|
||||
constructor(private readonly config: Config) {
|
||||
console.log("single tab driver", config.dbName);
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.worker.open(this.config.dbName, {
|
||||
async: this.config.async,
|
||||
encrypted: this.config.encrypted,
|
||||
url: this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
||||
});
|
||||
this.connection = new WaSqliteWorkerConnection(this.worker);
|
||||
}
|
||||
|
||||
async acquireConnection(): Promise<DatabaseConnection> {
|
||||
if (!this.connection) throw new Error("Driver not initialized.");
|
||||
|
||||
// SQLite only has one single connection. We use a mutex here to wait
|
||||
// until the single connection has been released.
|
||||
await this.connectionMutex.waitForUnlock();
|
||||
await this.connectionMutex.acquire();
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("begin"));
|
||||
}
|
||||
|
||||
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("commit"));
|
||||
}
|
||||
|
||||
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("rollback"));
|
||||
}
|
||||
|
||||
async releaseConnection(): Promise<void> {
|
||||
this.connectionMutex.release();
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
await this.worker.close();
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.worker.delete();
|
||||
}
|
||||
|
||||
async export() {
|
||||
return await this.worker.export();
|
||||
}
|
||||
}
|
||||
|
||||
class WaSqliteWorkerConnection implements DatabaseConnection {
|
||||
constructor(private readonly worker: SQLiteWorker) {}
|
||||
constructor(private readonly worker: SQLiteWorker | Remote<SQLiteWorker>) {}
|
||||
|
||||
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
||||
throw new Error("wasqlite driver doesn't support streaming");
|
||||
@@ -221,6 +281,8 @@ class WaSqliteWorkerConnection implements DatabaseConnection {
|
||||
: query.kind === "RawNode"
|
||||
? "raw"
|
||||
: "exec";
|
||||
return this.worker.run(mode, sql, parameters as any);
|
||||
return this.worker.run(mode, sql, parameters as any) as Promise<
|
||||
QueryResult<R>
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ type TaskProgress = {
|
||||
type ProgressReportCallback = (progress: TaskProgress) => void;
|
||||
|
||||
export class TaskManager {
|
||||
static async startTask<T>(task: TaskDefinition<T>): Promise<T> {
|
||||
static async startTask<T>(task: TaskDefinition<T>): Promise<T | Error> {
|
||||
switch (task.type) {
|
||||
case "status": {
|
||||
const statusTask = task;
|
||||
@@ -74,7 +74,7 @@ export class TaskManager {
|
||||
return result;
|
||||
}
|
||||
case "modal": {
|
||||
return await showProgressDialog<T>({
|
||||
return await showProgressDialog<T | Error>({
|
||||
title: task.title,
|
||||
subtitle: task.subtitle,
|
||||
action: task.action
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Cross,
|
||||
EditorFullWidth,
|
||||
EditorNormalWidth,
|
||||
@@ -34,9 +35,11 @@ import {
|
||||
Publish,
|
||||
Published,
|
||||
Readonly,
|
||||
Redo,
|
||||
Search,
|
||||
TableOfContents,
|
||||
Trash,
|
||||
Undo,
|
||||
Unlock
|
||||
} from "../icons";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
@@ -71,6 +74,8 @@ import { useStore as useMonographStore } from "../../stores/monograph-store";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { db } from "../../common/db";
|
||||
import { showPublishView } from "../publish-view";
|
||||
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
|
||||
export function EditorActionBar() {
|
||||
const editorMargins = useEditorStore((store) => store.editorMargins);
|
||||
@@ -79,19 +84,34 @@ export function EditorActionBar() {
|
||||
const activeSession = useEditorStore((store) =>
|
||||
store.activeSessionId ? store.getSession(store.activeSessionId) : undefined
|
||||
);
|
||||
const editor = useEditorManager((store) =>
|
||||
activeSession?.id ? store.editors[activeSession?.id]?.editor : undefined
|
||||
const editorManager = useEditorManager((store) =>
|
||||
activeSession?.id ? store.editors[activeSession?.id] : undefined
|
||||
);
|
||||
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
|
||||
const monographs = useMonographStore((store) => store.monographs);
|
||||
const isNotePublished =
|
||||
activeSession && db.monographs.isPublished(activeSession.id);
|
||||
const isMobile = useMobile();
|
||||
const setIsEditorOpen = useAppStore((store) => store.setIsEditorOpen);
|
||||
|
||||
const tools = [
|
||||
{
|
||||
title: "Undo",
|
||||
icon: Undo,
|
||||
enabled: editorManager?.canUndo,
|
||||
onClick: () => editorManager?.editor?.undo()
|
||||
},
|
||||
{
|
||||
title: "Redo",
|
||||
icon: Redo,
|
||||
enabled: editorManager?.canRedo,
|
||||
onClick: () => editorManager?.editor?.redo()
|
||||
},
|
||||
{
|
||||
title: isNotePublished ? "Published" : "Publish",
|
||||
icon: isNotePublished ? Published : Publish,
|
||||
hidden: !isLoggedIn,
|
||||
hideOnMobile: true,
|
||||
enabled:
|
||||
activeSession &&
|
||||
(activeSession.type === "default" || activeSession.type === "readonly"),
|
||||
@@ -105,6 +125,7 @@ export function EditorActionBar() {
|
||||
title: editorMargins ? "Disable editor margins" : "Enable editor margins",
|
||||
icon: editorMargins ? EditorNormalWidth : EditorFullWidth,
|
||||
enabled: true,
|
||||
hideOnMobile: true,
|
||||
onClick: () => useEditorStore.getState().toggleEditorMargins()
|
||||
},
|
||||
{
|
||||
@@ -154,7 +175,7 @@ export function EditorActionBar() {
|
||||
activeSession.type !== "locked" &&
|
||||
activeSession.type !== "diff" &&
|
||||
activeSession.type !== "conflicted",
|
||||
onClick: editor?.startSearch
|
||||
onClick: editorManager?.editor?.startSearch
|
||||
},
|
||||
{
|
||||
title: "Properties",
|
||||
@@ -172,7 +193,24 @@ export function EditorActionBar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabStrip />
|
||||
{isMobile ? (
|
||||
<Flex sx={{ flex: 1 }}>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
sx={{
|
||||
height: "100%",
|
||||
bg: "transparent",
|
||||
borderRadius: 0,
|
||||
flexShrink: 0
|
||||
}}
|
||||
onClick={() => setIsEditorOpen(false)}
|
||||
>
|
||||
<ArrowLeft size={18} />
|
||||
</Button>
|
||||
</Flex>
|
||||
) : (
|
||||
<TabStrip />
|
||||
)}
|
||||
{tools.map((tool) => (
|
||||
<Button
|
||||
data-test-id={tool.title}
|
||||
@@ -484,15 +522,11 @@ function Tab(props: TabProps) {
|
||||
e.stopPropagation();
|
||||
if (isTemporary) onKeepOpen();
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
if (e.button == 1) {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
onAuxClick={(e) => {
|
||||
if (e.button == 1) onClose();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFocus();
|
||||
onMouseUp={(e) => {
|
||||
if (e.button == 0) onFocus();
|
||||
}}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
@@ -523,9 +557,11 @@ function Tab(props: TabProps) {
|
||||
flexShrink: 0
|
||||
}}
|
||||
size={14}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPin();
|
||||
onMouseUp={(e) => {
|
||||
if (e.button == 0) {
|
||||
e.stopPropagation();
|
||||
onPin();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -536,9 +572,11 @@ function Tab(props: TabProps) {
|
||||
borderRadius: "default",
|
||||
flexShrink: 0
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
onMouseUp={(e) => {
|
||||
if (e.button == 0) {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
className="closeTabButton"
|
||||
size={16}
|
||||
@@ -593,6 +631,7 @@ function ReorderableList<T extends { id: string }>(
|
||||
measuring={{
|
||||
droppable: { strategy: MeasuringStrategy.Always }
|
||||
}}
|
||||
modifiers={[restrictToHorizontalAxis]}
|
||||
>
|
||||
<SortableContext items={items} strategy={horizontalListSortingStrategy}>
|
||||
{items.map((item, index) => (
|
||||
|
||||
@@ -127,11 +127,8 @@ export default function TabsView() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{IS_DESKTOP_APP ? (
|
||||
ReactDOM.createPortal(
|
||||
<EditorActionBar />,
|
||||
document.getElementById("titlebar-portal-container")!
|
||||
)
|
||||
{!hasNativeTitlebar ? (
|
||||
<EditorActionBarPortal />
|
||||
) : (
|
||||
<Flex sx={{ px: 1 }}>
|
||||
<EditorActionBar />
|
||||
@@ -153,12 +150,7 @@ export default function TabsView() {
|
||||
<PanelGroup direction="horizontal" autoSaveId={"editor-panels"}>
|
||||
<Panel id="editor-panel" className="editor-pane" order={1}>
|
||||
{sessions.map((session) => (
|
||||
<Freeze
|
||||
key={session.id}
|
||||
freeze={
|
||||
session.needsHydration || session.id !== activeSessionId
|
||||
}
|
||||
>
|
||||
<Freeze key={session.id} freeze={session.id !== activeSessionId}>
|
||||
{session.type === "locked" ? (
|
||||
<UnlockNoteView session={session} />
|
||||
) : session.type === "conflicted" || session.type === "diff" ? (
|
||||
@@ -233,7 +225,8 @@ const MemoizedEditorView = React.memo(
|
||||
EditorView,
|
||||
(prev, next) =>
|
||||
prev.session.id === next.session.id &&
|
||||
prev.session.type === next.session.type
|
||||
prev.session.type === next.session.type &&
|
||||
prev.session.needsHydration === next.session.needsHydration
|
||||
);
|
||||
function EditorView({
|
||||
session
|
||||
@@ -244,7 +237,7 @@ function EditorView({
|
||||
| ReadonlyEditorSession
|
||||
| DeletedEditorSession;
|
||||
}) {
|
||||
const lastChangedTime = useRef<number>(Date.now());
|
||||
const lastChangedTime = useRef<number>(0);
|
||||
const root = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toggleProperties = useEditorStore((store) => store.toggleProperties);
|
||||
@@ -302,6 +295,12 @@ function EditorView({
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session.needsHydration && session.content) {
|
||||
editor?.updateContent(session.content.data);
|
||||
}
|
||||
}, [editor, session.needsHydration]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={root}
|
||||
@@ -459,6 +458,7 @@ export function Editor(props: EditorProps) {
|
||||
<EditorChrome {...props}>
|
||||
<Tiptap
|
||||
id={id}
|
||||
isHydrating={!!session.needsHydration}
|
||||
nonce={nonce}
|
||||
readonly={readonly}
|
||||
content={content}
|
||||
@@ -466,7 +466,8 @@ export function Editor(props: EditorProps) {
|
||||
corsHost: Config.get("corsProxy", "https://cors.notesnook.com")
|
||||
}}
|
||||
onLoad={(editor) => {
|
||||
restoreSelection(editor, id);
|
||||
editor = editor || useEditorManager.getState().getEditor(id)?.editor;
|
||||
if (editor) restoreSelection(editor, id);
|
||||
restoreScrollPosition(session);
|
||||
}}
|
||||
onSelectionChange={({ from, to }) =>
|
||||
@@ -629,8 +630,8 @@ function EditorChrome(props: PropsWithChildren<EditorProps>) {
|
||||
maxWidth: editorMargins ? "min(100%, 850px)" : "auto",
|
||||
width: "100%"
|
||||
}}
|
||||
pl={6}
|
||||
pr={6}
|
||||
pl={[2, 2, 6]}
|
||||
pr={[2, 2, 6]}
|
||||
onClick={onRequestFocus}
|
||||
>
|
||||
{children}
|
||||
@@ -773,8 +774,10 @@ function restoreScrollPosition(session: EditorSession) {
|
||||
}
|
||||
|
||||
function restoreSelection(editor: IEditor, id: string) {
|
||||
editor.focus({
|
||||
position: Config.get(`${id}:selection`, { from: 0, to: 0 })
|
||||
setTimeout(() => {
|
||||
editor.focus({
|
||||
position: Config.get(`${id}:selection`, { from: 0, to: 0 })
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -811,7 +814,7 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
|
||||
throw new Error("note with this id does not exist.");
|
||||
|
||||
useEditorStore.getState().addSession({
|
||||
type: "default",
|
||||
type: session.note.readonly ? "readonly" : "default",
|
||||
locked: true,
|
||||
id: session.id,
|
||||
note: session.note,
|
||||
@@ -826,3 +829,9 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EditorActionBarPortal() {
|
||||
const container = document.getElementById("titlebar-portal-container");
|
||||
if (!container) return null;
|
||||
return ReactDOM.createPortal(<EditorActionBar />, container);
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ function TableOfContents(props: TableOfContentsProps) {
|
||||
key={t.id}
|
||||
sx={{
|
||||
textAlign: "left",
|
||||
paddingLeft: `${t.level * 5}px`,
|
||||
paddingLeft: `${t.level * 5 + (t.level - 1) * 5}px`,
|
||||
py: 1,
|
||||
pr: 1,
|
||||
borderLeft: "5px solid transparent",
|
||||
|
||||
@@ -59,6 +59,8 @@ import { writeToClipboard } from "../../utils/clipboard";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { parseInternalLink } from "@notesnook/core";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
import useTablet from "../../hooks/use-tablet";
|
||||
|
||||
export type OnChangeHandler = (
|
||||
content: () => string,
|
||||
@@ -67,7 +69,7 @@ export type OnChangeHandler = (
|
||||
type TipTapProps = {
|
||||
id: string;
|
||||
editorContainer: () => HTMLElement | undefined;
|
||||
onLoad?: (editor: IEditor) => void;
|
||||
onLoad?: (editor?: IEditor) => void;
|
||||
onChange?: OnChangeHandler;
|
||||
onContentChange?: () => void;
|
||||
onSelectionChange?: (range: { from: number; to: number }) => void;
|
||||
@@ -89,6 +91,7 @@ type TipTapProps = {
|
||||
readonly?: boolean;
|
||||
nonce?: number;
|
||||
isMobile?: boolean;
|
||||
isTablet?: boolean;
|
||||
downloadOptions?: DownloadOptions;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
@@ -127,6 +130,7 @@ function TipTap(props: TipTapProps) {
|
||||
readonly,
|
||||
nonce,
|
||||
isMobile,
|
||||
isTablet,
|
||||
downloadOptions,
|
||||
fontSize,
|
||||
fontFamily
|
||||
@@ -245,10 +249,13 @@ function TipTap(props: TipTapProps) {
|
||||
onDestroy: () => {
|
||||
useEditorManager.getState().setEditor(id);
|
||||
},
|
||||
onTransaction: ({ editor }) => {
|
||||
onTransaction: ({ editor, transaction }) => {
|
||||
useEditorManager.getState().updateEditor(id, {
|
||||
canRedo: editor.can().redo(),
|
||||
canUndo: editor.can().undo()
|
||||
canUndo: editor.can().undo(),
|
||||
tableOfContents: transaction.getMeta("isUpdatingContent")
|
||||
? getTableOfContents(editor.view.dom)
|
||||
: useEditorManager.getState().getEditor(id)?.tableOfContents
|
||||
});
|
||||
},
|
||||
copyToClipboard(text, html) {
|
||||
@@ -358,7 +365,12 @@ function TipTap(props: TipTapProps) {
|
||||
>
|
||||
<Toolbar
|
||||
editor={editor}
|
||||
location={isMobile ? "bottom" : "top"}
|
||||
location={"top"}
|
||||
sx={
|
||||
isTablet || isMobile
|
||||
? { overflowX: "scroll", flexWrap: "nowrap" }
|
||||
: {}
|
||||
}
|
||||
tools={toolbarConfig}
|
||||
defaultFontFamily={fontFamily}
|
||||
defaultFontSize={fontSize}
|
||||
@@ -371,14 +383,19 @@ function TipTap(props: TipTapProps) {
|
||||
function TiptapWrapper(
|
||||
props: PropsWithChildren<
|
||||
Omit<TipTapProps, "editorContainer" | "theme" | "fontSize" | "fontFamily">
|
||||
>
|
||||
> & {
|
||||
isHydrating?: boolean;
|
||||
}
|
||||
) {
|
||||
const { onLoad, isHydrating } = props;
|
||||
const theme = useThemeStore((store) =>
|
||||
store.colorScheme === "dark" ? store.darkTheme : store.lightTheme
|
||||
);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const editorContainerRef = useRef<HTMLDivElement>();
|
||||
const { editorConfig } = useEditorConfig();
|
||||
const isMobile = useMobile();
|
||||
const isTablet = useTablet();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (
|
||||
@@ -397,22 +414,37 @@ function TiptapWrapper(
|
||||
theme.scopes.base.primary.paragraph;
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHydrating) {
|
||||
onLoad?.();
|
||||
containerRef.current
|
||||
?.querySelector(".editor-loading-container")
|
||||
?.classList.add("hidden");
|
||||
}
|
||||
}, [isHydrating]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={containerRef}
|
||||
sx={{
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
".tiptap.ProseMirror": { pb: 150 }
|
||||
".tiptap.ProseMirror": { pb: 150 },
|
||||
".editor-container": { opacity: isHydrating ? 0 : 1 },
|
||||
".editor-loading-container.hidden": { display: "none" }
|
||||
}}
|
||||
>
|
||||
<TipTap
|
||||
{...props}
|
||||
isMobile={isMobile}
|
||||
isTablet={isTablet}
|
||||
onLoad={(editor) => {
|
||||
props.onLoad?.(editor);
|
||||
containerRef.current
|
||||
?.querySelector(".editor-loading-container")
|
||||
?.remove();
|
||||
if (!isHydrating) {
|
||||
onLoad?.(editor);
|
||||
containerRef.current
|
||||
?.querySelector(".editor-loading-container")
|
||||
?.classList.add("hidden");
|
||||
}
|
||||
}}
|
||||
editorContainer={() => {
|
||||
if (editorContainerRef.current) return editorContainerRef.current;
|
||||
@@ -471,6 +503,7 @@ function toIEditor(editor: Editor): IEditor {
|
||||
?.chain()
|
||||
.command(({ tr }) => {
|
||||
tr.setMeta("preventSave", true);
|
||||
tr.setMeta("isUpdatingContent", true);
|
||||
return true;
|
||||
})
|
||||
.setContent(content, false, { preserveWhitespace: true })
|
||||
|
||||
@@ -17,15 +17,42 @@ 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 { PropsWithChildren } from "react";
|
||||
import { PropsWithChildren, useEffect } from "react";
|
||||
import { ErrorText } from "../error-text";
|
||||
import { BaseThemeProvider } from "../theme-provider";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import {
|
||||
ErrorBoundary as RErrorBoundary,
|
||||
FallbackProps
|
||||
FallbackProps,
|
||||
useErrorBoundary
|
||||
} from "react-error-boundary";
|
||||
import { createDialect } from "../../common/sqlite";
|
||||
import { useKeyStore } from "../../interfaces/key-store";
|
||||
|
||||
export function GlobalErrorHandler(props: PropsWithChildren) {
|
||||
const { showBoundary } = useErrorBoundary();
|
||||
|
||||
useEffect(() => {
|
||||
function handleError(e: ErrorEvent) {
|
||||
const error = new Error(e.message);
|
||||
error.stack = `${e.filename}:${e.lineno}:${e.colno}`;
|
||||
showBoundary(e.error || error);
|
||||
}
|
||||
function handleUnhandledRejection(e: PromiseRejectionEvent) {
|
||||
showBoundary(e.reason);
|
||||
}
|
||||
window.addEventListener("unhandledrejection", handleUnhandledRejection);
|
||||
window.addEventListener("error", handleError);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"unhandledrejection",
|
||||
handleUnhandledRejection
|
||||
);
|
||||
window.removeEventListener("error", handleError);
|
||||
};
|
||||
}, [showBoundary]);
|
||||
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
export function ErrorBoundary(props: PropsWithChildren) {
|
||||
return (
|
||||
@@ -170,12 +197,11 @@ function getErrorHelp(props: FallbackProps) {
|
||||
return {
|
||||
explanation: `This error usually means the database file is either corrupt or it could not be decrypted.`,
|
||||
action:
|
||||
"This error can only be fixed by wiping & reseting the database. Beware that this will wipe all your data inside the database with no way to recover it later on.",
|
||||
"This error can only be fixed by wiping & reseting the database. Beware that this will wipe all your data inside the database with no way to recover it later on. This WILL NOT change/affect/delete/wipe your data on the server but ONLY on this device.",
|
||||
fix: async () => {
|
||||
const { useKeyStore } = await import("../../interfaces/key-store");
|
||||
|
||||
const { createDialect } = await import("../../common/sqlite");
|
||||
await useKeyStore.getState().clear();
|
||||
const dialect = createDialect("notesnook");
|
||||
const dialect = createDialect("notesnook", true);
|
||||
const driver = dialect.createDriver();
|
||||
await driver.delete();
|
||||
resetErrorBoundary();
|
||||
@@ -185,12 +211,11 @@ function getErrorHelp(props: FallbackProps) {
|
||||
return {
|
||||
explanation: `This error means the at rest encryption key could not be decrypted. This can be due to data corruption or implementation change.`,
|
||||
action:
|
||||
"This error can only be fixed by wiping & reseting the Key Store and the database.",
|
||||
"This error can only be fixed by wiping & reseting the Key Store and the database. This WILL NOT change/affect/delete/wipe your data on the server but ONLY on this device.",
|
||||
fix: async () => {
|
||||
const { useKeyStore } = await import("../../interfaces/key-store");
|
||||
|
||||
const { createDialect } = await import("../../common/sqlite");
|
||||
await useKeyStore.getState().clear();
|
||||
const dialect = createDialect("notesnook");
|
||||
const dialect = createDialect("notesnook", true);
|
||||
const driver = dialect.createDriver();
|
||||
await driver.delete();
|
||||
resetErrorBoundary();
|
||||
|
||||
@@ -340,9 +340,9 @@ export class Lightbox extends React.Component<LightboxProps> {
|
||||
overflow: "hidden",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
height: IS_DESKTOP_APP ? TITLE_BAR_HEIGHT : "auto",
|
||||
height: !hasNativeTitlebar ? TITLE_BAR_HEIGHT : "auto",
|
||||
pr:
|
||||
IS_DESKTOP_APP && getPlatform() !== "darwin"
|
||||
!hasNativeTitlebar && getPlatform() !== "darwin"
|
||||
? "calc(100vw - env(titlebar-area-width))"
|
||||
: 0
|
||||
}}
|
||||
|
||||
@@ -38,6 +38,7 @@ import ScrollContainer from "../scroll-container";
|
||||
import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
|
||||
import { VirtualizedGrouping, GroupingKey, Item } from "@notesnook/core";
|
||||
import {
|
||||
FlatScrollIntoViewLocation,
|
||||
ItemProps,
|
||||
ScrollerProps,
|
||||
Virtuoso,
|
||||
@@ -76,7 +77,6 @@ type ListContainerProps = {
|
||||
onClick: () => void;
|
||||
};
|
||||
};
|
||||
var activeItem: { focus: boolean; id: string } | undefined = undefined;
|
||||
function ListContainer(props: ListContainerProps) {
|
||||
const { group, items, context, refresh, header, button, compact } = props;
|
||||
|
||||
@@ -92,6 +92,7 @@ function ListContainer(props: ListContainerProps) {
|
||||
|
||||
const listRef = useRef<VirtuosoHandle>(null);
|
||||
const listContainerRef = useRef(null);
|
||||
const activeItem = useRef<{ focus: boolean; id: string }>();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -100,18 +101,18 @@ function ListContainer(props: ListContainerProps) {
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (activeItem) {
|
||||
if (activeItem.current) {
|
||||
items
|
||||
.ids()
|
||||
.then(
|
||||
(ids) =>
|
||||
listRef.current &&
|
||||
activeItem &&
|
||||
activeItem.current &&
|
||||
revealItemInList(
|
||||
listRef.current,
|
||||
activeItem.id,
|
||||
activeItem.current.id,
|
||||
ids,
|
||||
activeItem.focus
|
||||
activeItem.current.focus
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -119,8 +120,8 @@ function ListContainer(props: ListContainerProps) {
|
||||
const event = AppEventManager.subscribe(
|
||||
AppEvents.revealItemInList,
|
||||
(id, focus) => {
|
||||
if (activeItem?.id === id) return;
|
||||
activeItem = { id, focus };
|
||||
if (activeItem.current?.id === id) return;
|
||||
activeItem.current = { id, focus };
|
||||
items
|
||||
.ids()
|
||||
.then(
|
||||
@@ -135,7 +136,7 @@ function ListContainer(props: ListContainerProps) {
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
const { onFocus, onMouseDown, onKeyDown } = useKeyboardListNavigation({
|
||||
const { onMouseUp, onKeyDown } = useKeyboardListNavigation({
|
||||
length: items.length,
|
||||
reset: () => toggleSelection(false),
|
||||
deselect: (index) => {
|
||||
@@ -227,8 +228,7 @@ function ListContainer(props: ListContainerProps) {
|
||||
focusGroup: setFocusedGroupIndex,
|
||||
context,
|
||||
compact,
|
||||
onMouseDown,
|
||||
onFocus
|
||||
onMouseUp
|
||||
}}
|
||||
itemContent={(index, _data, context) => (
|
||||
<ItemRenderer context={context} index={index} />
|
||||
@@ -256,7 +256,7 @@ function ListContainer(props: ListContainerProps) {
|
||||
height: 45
|
||||
}}
|
||||
>
|
||||
<Plus color="static" />
|
||||
<Plus color="accentForeground" />
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -279,8 +279,7 @@ type ListContext = {
|
||||
context?: Context;
|
||||
compact?: boolean;
|
||||
|
||||
onMouseDown: (e: MouseEvent, itemIndex: number) => void;
|
||||
onFocus: (itemIndex: number) => void;
|
||||
onMouseUp: (e: MouseEvent, itemIndex: number) => void;
|
||||
};
|
||||
function ItemRenderer({
|
||||
index,
|
||||
@@ -436,9 +435,8 @@ function VirtuosoItem({
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
onFocus={() => context?.onFocus(props["data-item-index"])}
|
||||
onMouseDown={(e) =>
|
||||
context?.onMouseDown(e.nativeEvent, props["data-item-index"])
|
||||
onMouseUp={(e) =>
|
||||
context?.onMouseUp(e.nativeEvent, props["data-item-index"])
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
@@ -461,11 +459,13 @@ function waitForElement(
|
||||
list: VirtuosoHandle,
|
||||
index: number,
|
||||
elementId: string,
|
||||
callback: (element: HTMLElement) => void
|
||||
callback: (element: HTMLElement) => void,
|
||||
options?: Partial<FlatScrollIntoViewLocation>
|
||||
) {
|
||||
let waitInterval = 0;
|
||||
let maxAttempts = 3;
|
||||
list.scrollIntoView({
|
||||
...options,
|
||||
index,
|
||||
done: function scrollDone() {
|
||||
if (!maxAttempts) return;
|
||||
@@ -497,6 +497,7 @@ function revealItemInList(
|
||||
list,
|
||||
index,
|
||||
`id_${itemId}`,
|
||||
(element) => focus && element.focus()
|
||||
(element) => focus && element.focus(),
|
||||
{ align: "center" }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,28 +27,6 @@ const Lines = [1, 2].map(() => getRandomArbitrary(40, 90));
|
||||
export const ListLoader = memo(function ListLoader() {
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
sx={{ py: 1, alignItems: "center", justifyContent: "center", px: 1 }}
|
||||
>
|
||||
<Box sx={{ height: 38 }}>
|
||||
<Skeleton enableAnimation={false} width={38} height={38} circle />
|
||||
</Box>
|
||||
<Flex
|
||||
sx={{
|
||||
flex: 1,
|
||||
ml: 1,
|
||||
flexDirection: "column",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Box sx={{ height: 14 }}>
|
||||
<Skeleton enableAnimation={false} inline height={14} />
|
||||
</Box>
|
||||
<Box sx={{ mt: 1, height: 10 }}>
|
||||
<Skeleton enableAnimation={false} inline height={10} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{Lines.map((width) => (
|
||||
<Box key={width} sx={{ py: 2, px: 1 }}>
|
||||
<Skeleton
|
||||
|
||||
@@ -71,6 +71,7 @@ import { usePersistentState } from "../../hooks/use-persistent-state";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { Notebook, Tag } from "@notesnook/core";
|
||||
import { handleDrop } from "../../common/drop-handler";
|
||||
import { Menu } from "../../hooks/use-menu";
|
||||
|
||||
type Route = {
|
||||
id: string;
|
||||
@@ -172,6 +173,24 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
|
||||
const getSidebarItems = useCallback(async () => {
|
||||
return [
|
||||
{
|
||||
key: "reset-sidebar",
|
||||
type: "button",
|
||||
title: "Reset sidebar",
|
||||
onClick: () => {
|
||||
db.settings
|
||||
.setSideBarHiddenItems("routes", [])
|
||||
.then(() => db.settings.setSideBarHiddenItems("colors", []))
|
||||
.then(() => db.settings.setSideBarOrder("colors", []))
|
||||
.then(() => db.settings.setSideBarOrder("routes", []))
|
||||
.then(() => db.settings.setSideBarOrder("shortcuts", []))
|
||||
.then(() => {
|
||||
setHiddenRoutes([]);
|
||||
setHiddenColors([]);
|
||||
});
|
||||
}
|
||||
},
|
||||
{ type: "separator", key: "sep" },
|
||||
...toMenuItems(
|
||||
orderItems(routes, db.settings.getSideBarOrder("routes")),
|
||||
hiddenRoutes,
|
||||
@@ -180,7 +199,7 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
.setSideBarHiddenItems("routes", ids)
|
||||
.then(() => setHiddenRoutes(ids))
|
||||
),
|
||||
{ type: "separator", key: "sep" },
|
||||
{ type: "separator", key: "sep", isHidden: colors.length <= 0 },
|
||||
...toMenuItems(
|
||||
orderItems(colors, db.settings.getSideBarOrder("colors")),
|
||||
hiddenColors,
|
||||
@@ -225,6 +244,10 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
px={0}
|
||||
onContextMenu={async (e) => {
|
||||
e.preventDefault();
|
||||
Menu.openMenu(await getSidebarItems());
|
||||
}}
|
||||
>
|
||||
<FlexScrollContainer
|
||||
style={{
|
||||
|
||||
@@ -116,6 +116,7 @@ function NavigationItem(
|
||||
onContextMenu={(e) => {
|
||||
if (!menuItems) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
Menu.openMenu(menuItems);
|
||||
}}
|
||||
onClick={() => {
|
||||
|
||||
@@ -96,6 +96,7 @@ import { Context } from "../list-container/types";
|
||||
import { SchemeColors } from "@notesnook/theme";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
import Vault from "../../common/vault";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
|
||||
type NoteProps = NoteResolvedData & {
|
||||
item: NoteType;
|
||||
@@ -335,6 +336,7 @@ const menuItems: (
|
||||
ids?: string[],
|
||||
context?: { color?: Color; locked?: boolean }
|
||||
) => MenuItem[] = (note, ids = [], context) => {
|
||||
const isPro = isUserPremium();
|
||||
// const isSynced = db.notes.note(note.id)?.synced();
|
||||
|
||||
return [
|
||||
@@ -371,6 +373,7 @@ const menuItems: (
|
||||
//isDisabled: !isSynced,
|
||||
title: "Lock",
|
||||
isChecked: context?.locked,
|
||||
isDisabled: !isPro,
|
||||
icon: Lock.path,
|
||||
onClick: async () => {
|
||||
const { unlock, lock } = store.get();
|
||||
@@ -380,8 +383,7 @@ const menuItems: (
|
||||
} else if (await unlock(note.id)) {
|
||||
showToast("success", "Note unlocked successfully!");
|
||||
}
|
||||
},
|
||||
isPro: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
@@ -458,11 +460,12 @@ const menuItems: (
|
||||
title: format.title,
|
||||
tooltip: `Export as ${format.title} - ${format.subtitle}`,
|
||||
icon: format.icon.path,
|
||||
isDisabled: format.type === "pdf" && ids.length > 1,
|
||||
isDisabled:
|
||||
(format.type !== "txt" && !isPro) ||
|
||||
(format.type === "pdf" && ids.length > 1),
|
||||
// ? "Multiple notes cannot be exported as PDF."
|
||||
// : false,
|
||||
multiSelect: true,
|
||||
isPro: format.type !== "txt",
|
||||
onClick: async () => {
|
||||
if (ids.length === 1) {
|
||||
return await exportNote(note, {
|
||||
@@ -477,8 +480,7 @@ const menuItems: (
|
||||
}
|
||||
}))
|
||||
},
|
||||
multiSelect: true,
|
||||
isPro: true
|
||||
multiSelect: true
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
@@ -650,7 +652,8 @@ function notebooksMenuItems(ids: string[]): MenuItem[] {
|
||||
}
|
||||
});
|
||||
|
||||
menuItems.push({ key: "sep3", type: "separator" });
|
||||
if (notebookShortcuts.size > 0 || notebooks.size > 0)
|
||||
menuItems.push({ key: "sep3", type: "separator" });
|
||||
|
||||
if (notebookShortcuts.size > 0) {
|
||||
notebookShortcuts.forEach((notebook) => {
|
||||
|
||||
@@ -31,6 +31,7 @@ function Notice() {
|
||||
if (!notices) return null;
|
||||
return notices.slice().sort((a, b) => a.priority - b.priority)[0];
|
||||
}, [notices]);
|
||||
|
||||
if (!notice) return null;
|
||||
const NoticeData = NoticesData[notice.type];
|
||||
return (
|
||||
@@ -39,8 +40,8 @@ function Notice() {
|
||||
cursor: "pointer",
|
||||
borderRadius: "default",
|
||||
":hover": { bg: "hover" },
|
||||
alignItems: "center",
|
||||
minWidth: 250
|
||||
alignItems: "center"
|
||||
// minWidth: 250
|
||||
}}
|
||||
p={1}
|
||||
onClick={() => NoticeData.action()}
|
||||
@@ -52,11 +53,30 @@ function Notice() {
|
||||
color="accent"
|
||||
sx={{ bg: "shade", mr: 2, p: 2, borderRadius: 80 }}
|
||||
/>
|
||||
<Flex variant="columnCenter" sx={{ alignItems: "flex-start" }}>
|
||||
<Text variant="body" sx={{ fontSize: "body" }}>
|
||||
<Flex
|
||||
variant="columnCenter"
|
||||
sx={{ alignItems: "flex-start", overflow: "hidden" }}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: "body",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
{NoticeData.title}
|
||||
</Text>
|
||||
<Text variant="subBody" sx={{ display: "block" }}>
|
||||
<Text
|
||||
variant="subBody"
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
{NoticeData.subtitle}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -239,6 +239,10 @@ export function PdfPreview(props: PdfPreviewProps) {
|
||||
onZoom={(e) => {
|
||||
if (hash) setPDFConfig(hash, { scale: e.scale });
|
||||
}}
|
||||
transformGetDocumentParams={(options) => {
|
||||
(options as any).isEvalSupported = false;
|
||||
return options;
|
||||
}}
|
||||
// onDocumentAskPassword={(e) => {
|
||||
// e.verifyPassword("failed");
|
||||
// }}
|
||||
|
||||
@@ -57,7 +57,7 @@ function Placeholder(props: PlaceholderProps) {
|
||||
</Flex>
|
||||
|
||||
<Text variant="subBody" sx={{ fontSize: "body", mt: 1 }}>
|
||||
{toTitleCase(syncStatus.type || "syncing")}ing {syncStatus.progress}{" "}
|
||||
{toTitleCase(syncStatus.type || "sync")}ing {syncStatus.progress}{" "}
|
||||
items
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -23,9 +23,9 @@ import { Cross, Check, Loading } from "../../components/icons";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { ReactComponent as Rocket } from "../../assets/rocket.svg";
|
||||
import { ReactComponent as WorkAnywhere } from "../../assets/workanywhere.svg";
|
||||
import { ReactComponent as WorkLate } from "../../assets/worklate.svg";
|
||||
import Rocket from "../../assets/rocket.svg?react";
|
||||
import WorkAnywhere from "../../assets/workanywhere.svg?react";
|
||||
import WorkLate from "../../assets/worklate.svg?react";
|
||||
import Field from "../../components/field";
|
||||
import { hardNavigate } from "../../navigation";
|
||||
import { Features } from "./features";
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Text, Flex, Button } from "@theme-ui/components";
|
||||
import { Loading } from "../../components/icons";
|
||||
import { ReactComponent as Nomad } from "../../assets/nomad.svg";
|
||||
import Nomad from "../../assets/nomad.svg?react";
|
||||
import { Period, Plan } from "./types";
|
||||
import { PLAN_METADATA, usePlans } from "./plans";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -89,6 +89,8 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
)
|
||||
}
|
||||
]
|
||||
: IS_DESKTOP_APP
|
||||
? []
|
||||
: [],
|
||||
cta: {
|
||||
title: "Got it",
|
||||
|
||||
@@ -46,8 +46,8 @@ import { phone } from "phone";
|
||||
import { db } from "../../common/db";
|
||||
import FileSaver from "file-saver";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
import { ReactComponent as MFA } from "../../assets/mfa.svg";
|
||||
import { ReactComponent as Fallback2FA } from "../../assets/fallback2fa.svg";
|
||||
import MFA from "../../assets/mfa.svg?react";
|
||||
import Fallback2FA from "../../assets/fallback2fa.svg?react";
|
||||
import {
|
||||
Authenticator,
|
||||
StepComponent,
|
||||
|
||||
@@ -27,11 +27,11 @@ import {
|
||||
Github,
|
||||
Loading
|
||||
} from "../components/icons";
|
||||
import { ReactComponent as E2E } from "../assets/e2e.svg";
|
||||
import { ReactComponent as Note } from "../assets/note2.svg";
|
||||
import { ReactComponent as Nomad } from "../assets/nomad.svg";
|
||||
import { ReactComponent as WorkAnywhere } from "../assets/workanywhere.svg";
|
||||
import { ReactComponent as Friends } from "../assets/cause.svg";
|
||||
import E2E from "../assets/e2e.svg?react";
|
||||
import Note from "../assets/note2.svg?react";
|
||||
import Nomad from "../assets/nomad.svg?react";
|
||||
import WorkAnywhere from "../assets/workanywhere.svg?react";
|
||||
import Friends from "../assets/cause.svg?react";
|
||||
import LightUI from "../assets/light1.png";
|
||||
import DarkUI from "../assets/dark1.png";
|
||||
import GooglePlay from "../assets/play.png";
|
||||
|
||||
@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { SettingsGroup } from "./types";
|
||||
import { useStore as useSettingStore } from "../../stores/setting-store";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { desktop } from "../../common/desktop-bridge";
|
||||
|
||||
export const DesktopIntegrationSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -126,6 +128,42 @@ export const DesktopIntegrationSettings: SettingsGroup[] = [
|
||||
})
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "use-native-titlebar",
|
||||
title: "Use native titlebar",
|
||||
description:
|
||||
"Use native OS titlebar instead of replacing it with a custom one. Requires app restart for changes to take effect.",
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe(
|
||||
(s) => s.desktopIntegrationSettings,
|
||||
listener
|
||||
),
|
||||
components: [
|
||||
{
|
||||
type: "toggle",
|
||||
isToggled: () =>
|
||||
!!useSettingStore.getState().desktopIntegrationSettings
|
||||
?.nativeTitlebar,
|
||||
toggle: () => {
|
||||
useSettingStore.getState().setDesktopIntegration({
|
||||
nativeTitlebar:
|
||||
!useSettingStore.getState().desktopIntegrationSettings
|
||||
?.nativeTitlebar
|
||||
});
|
||||
showToast(
|
||||
"success",
|
||||
"Restart the app for changes to take effect.",
|
||||
[
|
||||
{
|
||||
text: "Restart now",
|
||||
onClick: () => desktop?.integration.restart.query()
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -135,7 +135,8 @@ export const ProfileSettings: SettingsGroup[] = [
|
||||
{
|
||||
key: "logout",
|
||||
title: "Logout",
|
||||
description: "Logging out will clear all data on this device.",
|
||||
description:
|
||||
"Logging out will clear all data stored on THIS DEVICE. Make sure you have synced all your changes before logging out.",
|
||||
keywords: [],
|
||||
components: [
|
||||
{
|
||||
|
||||
1
apps/web/src/global.d.ts
vendored
1
apps/web/src/global.d.ts
vendored
@@ -32,6 +32,7 @@ declare global {
|
||||
var IS_BETA: boolean;
|
||||
var APP_TITLE: string;
|
||||
var IS_THEME_BUILDER: boolean;
|
||||
var hasNativeTitlebar: boolean;
|
||||
|
||||
interface AuthenticationExtensionsClientInputs {
|
||||
prf?: {
|
||||
|
||||
@@ -17,50 +17,12 @@ 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 { useEffect, useState } from "react";
|
||||
import { initializeDatabase } from "../common/db";
|
||||
import { useErrorBoundary } from "react-error-boundary";
|
||||
import "../utils/analytics";
|
||||
import "../app.css";
|
||||
|
||||
// if (import.meta.env.PROD) {
|
||||
// console.log = () => {};
|
||||
// }
|
||||
|
||||
const memory = {
|
||||
isDatabaseLoaded: false
|
||||
};
|
||||
export default function useDatabase(persistence: "db" | "memory" = "db") {
|
||||
const [isAppLoaded, setIsAppLoaded] = useState(memory.isDatabaseLoaded);
|
||||
const { showBoundary } = useErrorBoundary();
|
||||
|
||||
useEffect(() => {
|
||||
loadDatabase(persistence)
|
||||
.then(() => setIsAppLoaded(true))
|
||||
.catch((e) => showBoundary(e));
|
||||
|
||||
function handleError(e: ErrorEvent) {
|
||||
const error = new Error(e.message);
|
||||
error.stack = `${e.filename}:${e.lineno}:${e.colno}`;
|
||||
showBoundary(e.error || error);
|
||||
}
|
||||
function handleUnhandledRejection(e: PromiseRejectionEvent) {
|
||||
showBoundary(e.reason);
|
||||
}
|
||||
window.addEventListener("unhandledrejection", handleUnhandledRejection);
|
||||
window.addEventListener("error", handleError);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"unhandledrejection",
|
||||
handleUnhandledRejection
|
||||
);
|
||||
window.removeEventListener("error", handleError);
|
||||
};
|
||||
}, [persistence]);
|
||||
|
||||
return [isAppLoaded];
|
||||
}
|
||||
|
||||
export async function loadDatabase(persistence: "db" | "memory" = "db") {
|
||||
if (memory.isDatabaseLoaded) return;
|
||||
|
||||
|
||||
@@ -62,10 +62,6 @@ export function useKeyboardListNavigation(
|
||||
: DIRECTION.UP;
|
||||
}, []);
|
||||
|
||||
const onFocus = useCallback((itemIndex: number) => {
|
||||
cursor.current = itemIndex;
|
||||
}, []);
|
||||
|
||||
const resetSelection = useCallback(() => {
|
||||
reset();
|
||||
anchor.current = -1;
|
||||
@@ -79,8 +75,9 @@ export function useKeyboardListNavigation(
|
||||
return true;
|
||||
}, [open, resetSelection, select]);
|
||||
|
||||
const onMouseDown = useCallback(
|
||||
const onMouseUp = useCallback(
|
||||
(e: MouseEvent, itemIndex: number) => {
|
||||
if (e.button !== 0) return;
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
select(itemIndex, true);
|
||||
} else if (e.shiftKey) {
|
||||
@@ -94,11 +91,12 @@ export function useKeyboardListNavigation(
|
||||
indices.push(i);
|
||||
}
|
||||
bulkSelect(indices);
|
||||
focusItemAt(endIndex);
|
||||
} else if (e.button === 0) {
|
||||
focusItemAt(itemIndex);
|
||||
} else {
|
||||
resetSelection();
|
||||
select(itemIndex);
|
||||
}
|
||||
cursor.current = itemIndex;
|
||||
},
|
||||
[select, resetSelection, bulkSelect, skip, focusItemAt]
|
||||
);
|
||||
@@ -117,6 +115,7 @@ export function useKeyboardListNavigation(
|
||||
while (skip && skip(nextIndex))
|
||||
nextIndex = moveUpCyclic(nextIndex, max);
|
||||
focusItemAt(nextIndex);
|
||||
cursor.current = nextIndex;
|
||||
return true;
|
||||
},
|
||||
ArrowDown: () => {
|
||||
@@ -126,6 +125,7 @@ export function useKeyboardListNavigation(
|
||||
while (skip && skip(nextIndex))
|
||||
nextIndex = moveDownCyclic(nextIndex, max);
|
||||
focusItemAt(nextIndex);
|
||||
cursor.current = nextIndex;
|
||||
return true;
|
||||
},
|
||||
"Mod-a": () => {
|
||||
@@ -148,9 +148,11 @@ export function useKeyboardListNavigation(
|
||||
if (nextIndex === cursor.current) return false;
|
||||
|
||||
focusItemAt(nextIndex);
|
||||
cursor.current = nextIndex;
|
||||
if (direction() === DIRECTION.UP) {
|
||||
select(nextIndex);
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
},
|
||||
"Shift-ArrowDown": () => {
|
||||
@@ -168,9 +170,11 @@ export function useKeyboardListNavigation(
|
||||
if (nextIndex === cursor.current) return false;
|
||||
|
||||
focusItemAt(nextIndex);
|
||||
cursor.current = nextIndex;
|
||||
if (direction() === DIRECTION.DOWN) {
|
||||
select(nextIndex);
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
},
|
||||
Escape: () => {
|
||||
@@ -185,13 +189,14 @@ export function useKeyboardListNavigation(
|
||||
resetSelection,
|
||||
skip,
|
||||
focusItemAt,
|
||||
select,
|
||||
bulkSelect,
|
||||
direction,
|
||||
select,
|
||||
deselect
|
||||
]
|
||||
);
|
||||
|
||||
return { onFocus, onMouseDown, onKeyDown };
|
||||
return { onMouseUp, onKeyDown };
|
||||
}
|
||||
|
||||
const moveDownCyclic = (i: number, max: number) => (i < max ? ++i : 0);
|
||||
|
||||
@@ -91,7 +91,6 @@ export default function useSlider(
|
||||
const slideToIndex = useCallback(
|
||||
(index: number) => {
|
||||
if (!slides || !ref.current || index >= slides.length) return;
|
||||
console.log(slides[index].offset, slides[index].width);
|
||||
const slider = ref.current;
|
||||
setTimeout(() => {
|
||||
slider.scrollTo({
|
||||
|
||||
@@ -50,6 +50,9 @@ export function useWindowControls() {
|
||||
isMaximized,
|
||||
isFullscreen,
|
||||
hasNativeWindowControls:
|
||||
!IS_DESKTOP_APP || getPlatform() === "darwin" || getPlatform() === "win32"
|
||||
!IS_DESKTOP_APP ||
|
||||
hasNativeTitlebar ||
|
||||
getPlatform() === "darwin" ||
|
||||
getPlatform() === "win32"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,28 +38,8 @@
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<title>Notesnook</title>
|
||||
|
||||
<style id="theme-colors">
|
||||
#splash {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
<script type="module">
|
||||
import { themeToCSS } from "@notesnook/theme";
|
||||
|
||||
const colorScheme = JSON.parse(
|
||||
window.localStorage.getItem("colorScheme") || '"light"'
|
||||
);
|
||||
const root = document.querySelector("html");
|
||||
if (root) root.setAttribute("data-theme", colorScheme);
|
||||
|
||||
const theme = window.localStorage.getItem(`theme:${colorScheme}`);
|
||||
if (theme) {
|
||||
const css = themeToCSS(JSON.parse(theme));
|
||||
const stylesheet = document.getElementById("theme-colors");
|
||||
if (stylesheet) stylesheet.innerHTML = css;
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
<style id="theme-colors"></style>
|
||||
<script type="module" src="./index.ts"></script>
|
||||
<style>
|
||||
html {
|
||||
overscroll-behavior: none;
|
||||
|
||||
68
apps/web/src/index.ts
Normal file
68
apps/web/src/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
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 "./app.css";
|
||||
import { AppEventManager, AppEvents } from "./common/app-events";
|
||||
import { register } from "./service-worker-registration";
|
||||
import { getServiceWorkerVersion } from "./utils/version";
|
||||
import { register as registerStreamSaver } from "./utils/stream-saver/mitm";
|
||||
import { ThemeDark, ThemeLight, themeToCSS } from "@notesnook/theme";
|
||||
import Config from "./utils/config";
|
||||
|
||||
const colorScheme = JSON.parse(
|
||||
window.localStorage.getItem("colorScheme") || '"light"'
|
||||
);
|
||||
const root = document.querySelector("html");
|
||||
if (root) root.setAttribute("data-theme", colorScheme);
|
||||
|
||||
const theme =
|
||||
colorScheme === "dark"
|
||||
? Config.get("theme:dark", ThemeDark)
|
||||
: Config.get("theme:light", ThemeLight);
|
||||
const stylesheet = document.getElementById("theme-colors");
|
||||
if (theme) {
|
||||
const css = themeToCSS(theme);
|
||||
if (stylesheet) stylesheet.innerHTML = css;
|
||||
} else stylesheet?.remove();
|
||||
|
||||
if (!IS_DESKTOP_APP && !IS_TESTING) {
|
||||
// logger.info("Initializing service worker...");
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
register({
|
||||
onUpdate: async (registration: ServiceWorkerRegistration) => {
|
||||
if (!registration.waiting) return;
|
||||
const { formatted } = await getServiceWorkerVersion(registration.waiting);
|
||||
AppEventManager.publish(AppEvents.updateDownloadCompleted, {
|
||||
version: formatted
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
registerStreamSaver();
|
||||
}
|
||||
});
|
||||
|
||||
// window.addEventListener("beforeinstallprompt", () => showInstallNotice());
|
||||
}
|
||||
|
||||
import("./root").then(({ startApp }) => {
|
||||
startApp();
|
||||
});
|
||||
@@ -1,103 +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 { createRoot } from "react-dom/client";
|
||||
import { Routes, init } from "./bootstrap";
|
||||
import { logger } from "./utils/logger";
|
||||
import { AppEventManager, AppEvents } from "./common/app-events";
|
||||
import { BaseThemeProvider } from "./components/theme-provider";
|
||||
import { register } from "./utils/stream-saver/mitm";
|
||||
import { getServiceWorkerVersion } from "./utils/version";
|
||||
import { ErrorBoundary, ErrorComponent } from "./components/error-boundary";
|
||||
import { TitleBar } from "./components/title-bar";
|
||||
|
||||
renderApp();
|
||||
|
||||
async function renderApp() {
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) return;
|
||||
const root = createRoot(rootElement);
|
||||
|
||||
try {
|
||||
const { component, props, path } = await init();
|
||||
|
||||
const { useKeyStore } = await import("./interfaces/key-store");
|
||||
await useKeyStore.getState().init();
|
||||
|
||||
if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
|
||||
|
||||
const { default: Component } = await component();
|
||||
const { default: AppLock } = await import("./views/app-lock");
|
||||
|
||||
root.render(
|
||||
<>
|
||||
{IS_DESKTOP_APP ? <TitleBar /> : null}
|
||||
<ErrorBoundary>
|
||||
<BaseThemeProvider
|
||||
onRender={() => document.getElementById("splash")?.remove()}
|
||||
sx={{ bg: "background", flex: 1, overflow: "hidden" }}
|
||||
>
|
||||
<AppLock>
|
||||
<Component route={props?.route || "login:email"} />
|
||||
</AppLock>
|
||||
</BaseThemeProvider>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
} catch (e) {
|
||||
root.render(
|
||||
<>
|
||||
{IS_DESKTOP_APP ? <TitleBar /> : null}
|
||||
<ErrorComponent
|
||||
error={e}
|
||||
resetErrorBoundary={() => window.location.reload()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const serviceWorkerWhitelist: Routes[] = ["default"];
|
||||
async function initializeServiceWorker() {
|
||||
if (!IS_DESKTOP_APP && !IS_TESTING) {
|
||||
logger.info("Initializing service worker...");
|
||||
const serviceWorker = await import("./service-worker-registration");
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.register({
|
||||
onUpdate: async (registration: ServiceWorkerRegistration) => {
|
||||
if (!registration.waiting) return;
|
||||
const { formatted } = await getServiceWorkerVersion(
|
||||
registration.waiting
|
||||
);
|
||||
AppEventManager.publish(AppEvents.updateDownloadCompleted, {
|
||||
version: formatted
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
register();
|
||||
}
|
||||
});
|
||||
// window.addEventListener("beforeinstallprompt", () => showInstallNotice());
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.hot) import.meta.hot.accept();
|
||||
@@ -296,12 +296,15 @@ async function singlePartUploadFile(
|
||||
console.log("Streaming file upload!");
|
||||
const { url, headers, signal } = requestOptions;
|
||||
|
||||
const uploadUrl = await fetch(url, {
|
||||
const uploadUrl: string | { error?: string } = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers,
|
||||
signal
|
||||
}).then((res) => (res.ok ? res.text() : null));
|
||||
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url.");
|
||||
}).then((res) => (res.ok ? res.text() : res.json()));
|
||||
if (typeof uploadUrl !== "string")
|
||||
throw new Error(
|
||||
uploadUrl.error || "Unable to resolve attachment upload url."
|
||||
);
|
||||
|
||||
const response = await axios.request({
|
||||
url: uploadUrl,
|
||||
@@ -354,6 +357,9 @@ async function multiPartUploadFile(
|
||||
throw new WrappedError("Could not initiate multi-part upload.", e);
|
||||
});
|
||||
|
||||
if (initiateMultiPartUpload.data.error)
|
||||
throw new Error(initiateMultiPartUpload.data.error);
|
||||
|
||||
uploadId = initiateMultiPartUpload.data.uploadId;
|
||||
const { parts } = initiateMultiPartUpload.data;
|
||||
|
||||
|
||||
@@ -123,8 +123,8 @@ const decoder = new TextDecoder();
|
||||
*/
|
||||
|
||||
class KeyStore extends BaseStore<KeyStore> {
|
||||
#secretStore: IKVStore;
|
||||
#metadataStore: IKVStore;
|
||||
#secretStore!: IKVStore;
|
||||
#metadataStore!: IKVStore;
|
||||
#keyId = "key";
|
||||
#wrappingKeyId = "wrappingKey";
|
||||
#key?: CryptoKey;
|
||||
@@ -134,25 +134,25 @@ class KeyStore extends BaseStore<KeyStore> {
|
||||
isLocked = false;
|
||||
|
||||
constructor(
|
||||
dbName: string,
|
||||
private readonly dbName: string,
|
||||
setState: SetState<KeyStore>,
|
||||
get: GetState<KeyStore>
|
||||
) {
|
||||
super(setState, get);
|
||||
|
||||
this.#metadataStore =
|
||||
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
|
||||
? new IndexedDBKVStore(`${dbName}-metadata`, "metadata")
|
||||
: new MemoryKVStore();
|
||||
this.#secretStore =
|
||||
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
|
||||
? new IndexedDBKVStore(`${dbName}-secrets`, "secrets")
|
||||
: new MemoryKVStore();
|
||||
}
|
||||
|
||||
activeCredentials = () => this.get().credentials.filter((c) => c.active);
|
||||
|
||||
init = async () => {
|
||||
this.#metadataStore =
|
||||
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
|
||||
? new IndexedDBKVStore(`${this.dbName}-metadata`, "metadata")
|
||||
: new MemoryKVStore();
|
||||
this.#secretStore =
|
||||
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
|
||||
? new IndexedDBKVStore(`${this.dbName}-secrets`, "secrets")
|
||||
: new MemoryKVStore();
|
||||
|
||||
const credentials = await this.getCredentials();
|
||||
const secrets = Object.fromEntries(
|
||||
await this.#secretStore.entries<EncryptedData>()
|
||||
|
||||
@@ -20,8 +20,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useState } from "react";
|
||||
import EventManager from "@notesnook/core/dist/utils/event-manager";
|
||||
import Config from "../utils/config";
|
||||
import { HashRoute } from "./hash-routes";
|
||||
import { ReplaceParametersInPath } from "./types";
|
||||
import type { HashRoute } from "./hash-routes";
|
||||
import type { ReplaceParametersInPath } from "./types";
|
||||
|
||||
export function navigate(
|
||||
url: string,
|
||||
|
||||
138
apps/web/src/root.tsx
Normal file
138
apps/web/src/root.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
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 { createRoot } from "react-dom/client";
|
||||
import { init, Routes } from "./bootstrap";
|
||||
import { BaseThemeProvider } from "./components/theme-provider";
|
||||
import {
|
||||
ErrorBoundary,
|
||||
ErrorComponent,
|
||||
GlobalErrorHandler
|
||||
} from "./components/error-boundary";
|
||||
import { TitleBar } from "./components/title-bar";
|
||||
import { desktop } from "./common/desktop-bridge";
|
||||
import { useKeyStore } from "./interfaces/key-store";
|
||||
import Config from "./utils/config";
|
||||
import { usePromise } from "@notesnook/common";
|
||||
import { AuthProps } from "./views/auth";
|
||||
|
||||
export async function startApp() {
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) return;
|
||||
const root = createRoot(rootElement);
|
||||
|
||||
window.hasNativeTitlebar =
|
||||
!IS_DESKTOP_APP ||
|
||||
!!(await desktop?.integration.desktopIntegration
|
||||
.query()
|
||||
?.then((s) => s.nativeTitlebar));
|
||||
|
||||
try {
|
||||
const { component, props, path } = await init();
|
||||
|
||||
await useKeyStore.getState().init();
|
||||
|
||||
const { default: AppLock } = await import("./views/app-lock");
|
||||
|
||||
root.render(
|
||||
<>
|
||||
{hasNativeTitlebar ? null : <TitleBar />}
|
||||
<ErrorBoundary>
|
||||
<GlobalErrorHandler>
|
||||
<BaseThemeProvider
|
||||
onRender={() => document.getElementById("splash")?.remove()}
|
||||
sx={{ bg: "background", flex: 1, overflow: "hidden" }}
|
||||
>
|
||||
<AppLock>
|
||||
<RouteWrapper
|
||||
component={component}
|
||||
path={path}
|
||||
routeProps={props}
|
||||
/>
|
||||
</AppLock>
|
||||
</BaseThemeProvider>
|
||||
</GlobalErrorHandler>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
root.render(
|
||||
<>
|
||||
{hasNativeTitlebar ? null : <TitleBar />}
|
||||
<ErrorComponent
|
||||
error={e}
|
||||
resetErrorBoundary={() => window.location.reload()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function RouteWrapper(props: {
|
||||
component: () => Promise<{
|
||||
default: (props: AuthProps) => JSX.Element;
|
||||
}>;
|
||||
path: Routes;
|
||||
routeProps: AuthProps | null;
|
||||
}) {
|
||||
const { component, path, routeProps } = props;
|
||||
const result = usePromise(async () => {
|
||||
await import("./hooks/use-database").then(({ loadDatabase }) =>
|
||||
loadDatabase(
|
||||
path !== "/sessionexpired" || Config.get("sessionExpired", false)
|
||||
? "db"
|
||||
: "memory"
|
||||
)
|
||||
);
|
||||
|
||||
const { default: Component } = await component();
|
||||
return Component;
|
||||
}, [component, path]);
|
||||
|
||||
if (result.status === "rejected") {
|
||||
throw result.reason instanceof Error
|
||||
? result.reason
|
||||
: new Error(result.reason);
|
||||
}
|
||||
if (result.status === "pending")
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "var(--background)",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<svg style={{ height: 120 }}>
|
||||
<use href="#themed-logo" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
return <result.value route={routeProps?.route || "login:email"} />;
|
||||
}
|
||||
|
||||
if (import.meta.hot) import.meta.hot.accept();
|
||||
@@ -21,7 +21,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { clientsClaim } from "workbox-core";
|
||||
import { ExpirationPlugin } from "workbox-expiration";
|
||||
import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
|
||||
import {
|
||||
precacheAndRoute,
|
||||
createHandlerBoundToURL,
|
||||
cleanupOutdatedCaches
|
||||
} from "workbox-precaching";
|
||||
import { registerRoute } from "workbox-routing";
|
||||
import { StaleWhileRevalidate } from "workbox-strategies";
|
||||
|
||||
@@ -29,15 +33,8 @@ declare var self: ServiceWorkerGlobalScope & typeof globalThis;
|
||||
|
||||
clientsClaim();
|
||||
|
||||
const precacheRoutes = self.__WB_MANIFEST;
|
||||
const filters = [/KaTeX/i, /hack/i, /code-lang-/i];
|
||||
precacheAndRoute(
|
||||
precacheRoutes.filter((route) => {
|
||||
return filters.every(
|
||||
(filter) => !filter.test(typeof route === "string" ? route : route.url)
|
||||
);
|
||||
})
|
||||
);
|
||||
cleanupOutdatedCaches();
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
// Set up App Shell-style routing, so that all navigation requests
|
||||
// are fulfilled with your index.html shell. Learn more at
|
||||
|
||||
@@ -198,10 +198,9 @@ class AppStore extends BaseStore<AppStore> {
|
||||
};
|
||||
|
||||
toggleSideMenu = (toggleState: boolean) => {
|
||||
console.log("toggling side menu");
|
||||
this.set(
|
||||
(state) =>
|
||||
(state.isSideMenuOpen =
|
||||
toggleState != null ? toggleState : !state.isSideMenuOpen)
|
||||
(state) => (state.isSideMenuOpen = toggleState ?? !state.isSideMenuOpen)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export type ReadonlyEditorSession = BaseEditorSession & {
|
||||
content?: NoteContent<false>;
|
||||
color?: string;
|
||||
tags?: Tag[];
|
||||
locked?: boolean;
|
||||
};
|
||||
|
||||
export type DeletedEditorSession = BaseEditorSession & {
|
||||
|
||||
@@ -89,6 +89,21 @@ async function processAttachment(
|
||||
attachments[name] = { ...cipherData, key };
|
||||
}
|
||||
|
||||
const colorMap: Record<string, string | undefined> = {
|
||||
default: undefined,
|
||||
teal: "#00897B",
|
||||
red: "#D32F2F",
|
||||
purple: "#7B1FA2",
|
||||
blue: "#1976D2",
|
||||
cerulean: "#03A9F4",
|
||||
pink: "#C2185B",
|
||||
brown: "#795548",
|
||||
gray: "#9E9E9E",
|
||||
green: "#388E3C",
|
||||
orange: "#FFA000",
|
||||
yellow: "#FFC107"
|
||||
};
|
||||
|
||||
async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
|
||||
const note = await fileToJson<Note>(entry);
|
||||
for (const attachment of note.attachments || []) {
|
||||
@@ -121,8 +136,49 @@ async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
|
||||
content: { type: "tiptap", data: note.content?.data },
|
||||
notebooks: []
|
||||
});
|
||||
|
||||
if (!noteId) return;
|
||||
|
||||
for (const tag of note.tags || []) {
|
||||
const tagId =
|
||||
(await db.tags.find(tag))?.id ||
|
||||
(await db.tags.add({
|
||||
title: tag
|
||||
}));
|
||||
|
||||
await db.relations.add(
|
||||
{
|
||||
id: tagId,
|
||||
type: "tag"
|
||||
},
|
||||
{
|
||||
id: noteId,
|
||||
type: "note"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const colorCode = note.color ? colorMap[note.color] : undefined;
|
||||
if (colorCode) {
|
||||
const colorId =
|
||||
(await db.colors.find(colorCode))?.id ||
|
||||
(await db.colors.add({
|
||||
colorCode: colorCode,
|
||||
title: note.color
|
||||
}));
|
||||
|
||||
await db.relations.add(
|
||||
{
|
||||
id: colorId,
|
||||
type: "color"
|
||||
},
|
||||
{
|
||||
id: noteId,
|
||||
type: "note"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (const nb of notebooks) {
|
||||
const notebookId = await importNotebook(nb).catch(() => undefined);
|
||||
if (!notebookId) continue;
|
||||
|
||||
@@ -44,7 +44,7 @@ async function initializeLogger() {
|
||||
synchronous: "normal",
|
||||
pageSize: 8192,
|
||||
cacheSize: -32000,
|
||||
skipInitialization: !IS_DESKTOP_APP
|
||||
skipInitialization: !IS_DESKTOP_APP && !!globalThis.SharedWorker
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
@@ -20,12 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useEffect } from "react";
|
||||
import { useStore } from "../stores/note-store";
|
||||
import ListContainer from "../components/list-container";
|
||||
import { hashNavigate } from "../navigation";
|
||||
import useNavigate from "../hooks/use-navigate";
|
||||
import Placeholder from "../components/placeholders";
|
||||
import { useSearch } from "../hooks/use-search";
|
||||
import { db } from "../common/db";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { ListLoader } from "../components/loaders/list-loader";
|
||||
|
||||
function Home() {
|
||||
const notes = useStore((store) => store.notes);
|
||||
@@ -60,7 +60,7 @@ function Home() {
|
||||
// })();
|
||||
// }, []);
|
||||
|
||||
if (!notes) return <Placeholder context="notes" />;
|
||||
if (!notes) return <ListLoader />;
|
||||
return (
|
||||
<ListContainer
|
||||
group="home"
|
||||
|
||||
@@ -34,7 +34,6 @@ import { getQueryParams, hardNavigate, makeURL } from "../navigation";
|
||||
import { store as userstore } from "../stores/user-store";
|
||||
import { db } from "../common/db";
|
||||
import Config from "../utils/config";
|
||||
import useDatabase from "../hooks/use-database";
|
||||
import { Loader } from "../components/loader";
|
||||
import { showToast } from "../utils/toast";
|
||||
import AuthContainer from "../components/auth-container";
|
||||
@@ -173,16 +172,13 @@ function Auth(props: AuthProps) {
|
||||
window.history.replaceState({}, "", makeURL(routePaths[route]));
|
||||
}, [route]);
|
||||
|
||||
const [isAppLoaded] = useDatabase();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAppLoaded) return;
|
||||
db.user.getUser().then((user) => {
|
||||
if (user && authorizedRoutes.includes(route) && !isSessionExpired())
|
||||
return openURL("/");
|
||||
setIsReady(true);
|
||||
});
|
||||
}, [isAppLoaded, route]);
|
||||
}, [route]);
|
||||
|
||||
if (!isReady) return <></>;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import Placeholder from "../components/placeholders";
|
||||
import { useEffect } from "react";
|
||||
import { db } from "../common/db";
|
||||
import { useSearch } from "../hooks/use-search";
|
||||
import { ListLoader } from "../components/loaders/list-loader";
|
||||
|
||||
function Notebooks() {
|
||||
const notebooks = useStore((state) => state.notebooks);
|
||||
@@ -36,7 +37,7 @@ function Notebooks() {
|
||||
store.get().refresh();
|
||||
}, []);
|
||||
|
||||
if (!notebooks) return <Placeholder context="notebooks" />;
|
||||
if (!notebooks) return <ListLoader />;
|
||||
return (
|
||||
<>
|
||||
<ListContainer
|
||||
|
||||
@@ -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 { useEffect } from "react";
|
||||
import ListContainer from "../components/list-container";
|
||||
import {
|
||||
notesFromContext,
|
||||
@@ -28,6 +27,7 @@ import { useSearch } from "../hooks/use-search";
|
||||
import { db } from "../common/db";
|
||||
import { handleDrop } from "../common/drop-handler";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { ListLoader } from "../components/loaders/list-loader";
|
||||
|
||||
type NotesProps = { header?: JSX.Element };
|
||||
function Notes(props: NotesProps) {
|
||||
@@ -47,7 +47,7 @@ function Notes(props: NotesProps) {
|
||||
[context, contextNotes]
|
||||
);
|
||||
|
||||
if (!context || !contextNotes) return <Placeholder context="notes" />;
|
||||
if (!context || !contextNotes) return <ListLoader />;
|
||||
return (
|
||||
<ListContainer
|
||||
group={type}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { makeURL, useQueryParams } from "../navigation";
|
||||
import { db } from "../common/db";
|
||||
import useDatabase from "../hooks/use-database";
|
||||
import { Loader } from "../components/loader";
|
||||
import { showToast } from "../utils/toast";
|
||||
import AuthContainer from "../components/auth-container";
|
||||
@@ -122,11 +121,9 @@ function useAuthenticateUser({
|
||||
code: string;
|
||||
userId: string;
|
||||
}) {
|
||||
const [isAppLoaded] = useDatabase(isSessionExpired() ? "db" : "memory");
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(true);
|
||||
const [user, setUser] = useState<User>();
|
||||
useEffect(() => {
|
||||
if (!isAppLoaded) return;
|
||||
async function authenticateUser() {
|
||||
setIsAuthenticating(true);
|
||||
try {
|
||||
@@ -148,7 +145,7 @@ function useAuthenticateUser({
|
||||
}
|
||||
|
||||
authenticateUser();
|
||||
}, [code, userId, isAppLoaded]);
|
||||
}, [code, userId]);
|
||||
return { isAuthenticating, user };
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import useNavigate from "../hooks/use-navigate";
|
||||
import Placeholder from "../components/placeholders";
|
||||
import { db } from "../common/db";
|
||||
import { useSearch } from "../hooks/use-search";
|
||||
import { ListLoader } from "../components/loaders/list-loader";
|
||||
|
||||
function Reminders() {
|
||||
useNavigate("reminders", () => store.refresh());
|
||||
@@ -33,7 +34,7 @@ function Reminders() {
|
||||
db.lookup.reminders(query).sorted()
|
||||
);
|
||||
|
||||
if (!reminders) return <Placeholder context="reminders" />;
|
||||
if (!reminders) return <ListLoader />;
|
||||
return (
|
||||
<>
|
||||
<ListContainer
|
||||
|
||||
@@ -23,6 +23,7 @@ import useNavigate from "../hooks/use-navigate";
|
||||
import Placeholder from "../components/placeholders";
|
||||
import { useSearch } from "../hooks/use-search";
|
||||
import { db } from "../common/db";
|
||||
import { ListLoader } from "../components/loaders/list-loader";
|
||||
|
||||
function Tags() {
|
||||
useNavigate("tags", () => store.refresh());
|
||||
@@ -32,7 +33,7 @@ function Tags() {
|
||||
db.lookup.tags(query).sorted()
|
||||
);
|
||||
|
||||
if (!tags) return <Placeholder context="tags" />;
|
||||
if (!tags) return <ListLoader />;
|
||||
return (
|
||||
<ListContainer
|
||||
group="tags"
|
||||
|
||||
@@ -25,6 +25,7 @@ import useNavigate from "../hooks/use-navigate";
|
||||
import Placeholder from "../components/placeholders";
|
||||
import { useSearch } from "../hooks/use-search";
|
||||
import { db } from "../common/db";
|
||||
import { ListLoader } from "../components/loaders/list-loader";
|
||||
|
||||
function Trash() {
|
||||
useNavigate("trash", store.refresh);
|
||||
@@ -35,7 +36,7 @@ function Trash() {
|
||||
db.lookup.trash(query).sorted()
|
||||
);
|
||||
|
||||
if (!items) return <Placeholder context="trash" />;
|
||||
if (!items) return <ListLoader />;
|
||||
return (
|
||||
<ListContainer
|
||||
group="trash"
|
||||
|
||||
@@ -44,7 +44,6 @@ const isTesting =
|
||||
const isDesktop = process.env.PLATFORM === "desktop";
|
||||
const isThemeBuilder = process.env.THEME_BUILDER === "true";
|
||||
const isAnalyzing = process.env.ANALYZING === "true";
|
||||
process.env.NN_BUILD_TIMESTAMP = isTesting ? "0" : `${Date.now()}`;
|
||||
|
||||
export default defineConfig({
|
||||
envPrefix: "NN_",
|
||||
@@ -61,7 +60,16 @@ export default defineConfig({
|
||||
output: {
|
||||
plugins: [emitEditorStyles()],
|
||||
assetFileNames: "assets/[name]-[hash:12][extname]",
|
||||
chunkFileNames: "assets/[name]-[hash:12].js"
|
||||
chunkFileNames: "assets/[name]-[hash:12].js",
|
||||
manualChunks: (id: string) => {
|
||||
if (
|
||||
(id.includes("/editor/languages/") ||
|
||||
id.includes("/html/languages/")) &&
|
||||
path.basename(id) !== "index.js"
|
||||
)
|
||||
return `code-lang-${path.basename(id, "js")}`;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -107,6 +115,8 @@ export default defineConfig({
|
||||
format: "es",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: "assets/[name]-[hash:12][extname]",
|
||||
chunkFileNames: "assets/[name]-[hash:12].js",
|
||||
inlineDynamicImports: true
|
||||
}
|
||||
}
|
||||
@@ -135,13 +145,30 @@ export default defineConfig({
|
||||
manifest: WEB_MANIFEST,
|
||||
injectRegister: null,
|
||||
srcDir: "",
|
||||
filename: "service-worker.ts"
|
||||
filename: "service-worker.ts",
|
||||
mode: "production",
|
||||
workbox: { mode: "production" },
|
||||
injectManifest: {
|
||||
globPatterns: ["**/*.{js,css,html,wasm}", "**/open-sans-*.woff2"],
|
||||
globIgnores: [
|
||||
"**/node_modules/**/*",
|
||||
"**/code-lang-*.js",
|
||||
"pdf.worker.min.js"
|
||||
]
|
||||
}
|
||||
})
|
||||
]),
|
||||
react({
|
||||
plugins: isTesting
|
||||
? undefined
|
||||
: [["swc-plugin-react-remove-properties", {}]]
|
||||
: [
|
||||
[
|
||||
"@swc/plugin-react-remove-properties",
|
||||
{
|
||||
properties: ["^data-test-id$"]
|
||||
}
|
||||
]
|
||||
]
|
||||
}),
|
||||
envCompatible({
|
||||
prefix: "NN_",
|
||||
@@ -149,7 +176,8 @@ export default defineConfig({
|
||||
}),
|
||||
svgrPlugin({
|
||||
svgrOptions: {
|
||||
icon: true
|
||||
icon: true,
|
||||
namedExport: "ReactComponent"
|
||||
// ...svgr options (https://react-svgr.com/docs/options/)
|
||||
}
|
||||
})
|
||||
|
||||
4
fastlane/metadata/android/en-US/changelogs/15089.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/15089.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
- Bring back undo/redo buttons in editor
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
5
fastlane/metadata/android/en-US/changelogs/15094.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/15094.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
- Fixed realtime sync issues
|
||||
- Fixed color popups not opening from main toolbar
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
2
packages/clipper/.npmignore
Normal file
2
packages/clipper/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!dist/**/*
|
||||
4
packages/clipper/package-lock.json
generated
4
packages/clipper/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/clipper",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/clipper",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/clipper",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "Web clipper core used by the Notesnook Web Clipper",
|
||||
"keywords": [
|
||||
"web-clipper"
|
||||
@@ -24,6 +24,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && yarn webpack -c webpack.config.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "playwright test",
|
||||
"postinstall": "patch-package",
|
||||
"watch": "tsc --watch"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user