web: improve web app loading performance by 10x (#5845)

* web: improve web app loading performance by 10x

* web: update test snapshots
This commit is contained in:
Abdullah Atta
2024-06-03 11:07:44 +05:00
committed by GitHub
parent a45b66d449
commit 382b5b0240
35 changed files with 4050 additions and 1665 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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);
@@ -194,7 +182,7 @@ function DesktopAppContents({
borderRight: "1px solid var(--separator)"
}}
>
{isAppLoaded && <CachedRouter />}
<CachedRouter />
</ScopedThemeProvider>
</Panel>
<PanelResizeHandle className="panel-resize-handle" />
@@ -208,7 +196,7 @@ function DesktopAppContents({
bg: "background"
}}
>
{isAppLoaded && <HashRouter />}
{<HashRouter />}
</Flex>
</Panel>
</PanelGroup>
@@ -218,7 +206,7 @@ function DesktopAppContents({
);
}
function MobileAppContents({ isAppLoaded }: { isAppLoaded: boolean }) {
function MobileAppContents() {
return (
<FlexScrollContainer
id="slider"
@@ -257,11 +245,7 @@ function MobileAppContents({ isAppLoaded }: { isAppLoaded: boolean }) {
width: "100vw"
}}
>
<SuspenseLoader
condition={isAppLoaded}
component={CachedRouter}
fallback={<ViewLoader />}
/>
<CachedRouter />
<Box
id="overlay"
sx={{
@@ -290,7 +274,7 @@ function MobileAppContents({ isAppLoaded }: { isAppLoaded: boolean }) {
<SuspenseLoader
fallback={<EditorLoader />}
component={HashRouter}
condition={isAppLoaded}
condition={true}
/>
</Flex>
</FlexScrollContainer>

View File

@@ -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 await import("./utils/logger").then(({ initializeLogger }) =>
initializeLogger()
);
const { path, route } = getRoute();
return { ...route, path };

View File

@@ -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" }
: {
@@ -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) {

View File

@@ -128,10 +128,7 @@ export default function TabsView() {
return (
<>
{!hasNativeTitlebar ? (
ReactDOM.createPortal(
<EditorActionBar />,
document.getElementById("titlebar-portal-container")!
)
<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
@@ -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 }) =>
@@ -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 })
});
});
}
@@ -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);
}

View File

@@ -67,7 +67,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;
@@ -371,8 +371,11 @@ 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
);
@@ -397,22 +400,35 @@ 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}
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;

View File

@@ -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 (
@@ -172,10 +199,9 @@ function getErrorHelp(props: FallbackProps) {
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.",
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();
@@ -187,10 +213,9 @@ function getErrorHelp(props: FallbackProps) {
action:
"This error can only be fixed by wiping & reseting the Key Store and the database.",
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();

View File

@@ -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

View File

@@ -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 (

View File

@@ -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>

View File

@@ -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";

View File

@@ -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";

View File

@@ -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,

View File

@@ -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";

View File

@@ -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;

View File

@@ -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
View 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();
});

View File

@@ -1,110 +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";
import { desktop } from "./common/desktop-bridge";
renderApp();
async function renderApp() {
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();
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(
<>
{hasNativeTitlebar ? null : <TitleBar />}
<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(
<>
{hasNativeTitlebar ? null : <TitleBar />}
<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();

View File

@@ -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,

122
apps/web/src/root.tsx Normal file
View File

@@ -0,0 +1,122 @@
/*
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 } from "./bootstrap";
// import { logger } from "./utils/logger";
// import { AppEventManager, AppEvents } from "./common/app-events";
import { BaseThemeProvider } from "./components/theme-provider";
// import { register as registerStreamSaver } from "./utils/stream-saver/mitm";
// import { getServiceWorkerVersion } from "./utils/version";
import {
ErrorBoundary,
ErrorComponent,
GlobalErrorHandler
} from "./components/error-boundary";
import { TitleBar } from "./components/title-bar";
import { desktop } from "./common/desktop-bridge";
// import { register } from "./service-worker-registration";
import { useKeyStore } from "./interfaces/key-store";
import Config from "./utils/config";
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();
await import("./hooks/use-database").then(({ loadDatabase }) =>
loadDatabase(
path !== "/sessionexpired" || Config.get("sessionExpired", false)
? "db"
: "memory"
)
);
const { default: Component } = await component();
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>
<Component route={props?.route || "login:email"} />
</AppLock>
</BaseThemeProvider>
</GlobalErrorHandler>
</ErrorBoundary>
</>
);
} catch (e) {
console.error(e);
root.render(
<>
{hasNativeTitlebar ? null : <TitleBar />}
<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...");
// // 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());
// }
// }
if (import.meta.hot) import.meta.hot.accept();

View File

@@ -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

View File

@@ -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"

View File

@@ -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 <></>;

View File

@@ -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

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { 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}

View File

@@ -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 };
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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/)
}
})

View File

@@ -43,9 +43,7 @@ export function template(data: TemplateData) {
? `<meta name="tags" content="${data.tags.join(", ")}" />`
: ""
}
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=${
process.env.NN_BUILD_TIMESTAMP || "1690887574068"
}">
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=1690887574068">
<style>