refactor: improve app startup perf many times

This commit is contained in:
thecodrr
2021-10-23 10:58:44 +05:00
parent 98998e27a4
commit 45d33d90f6
66 changed files with 1120 additions and 784 deletions

View File

@@ -7,8 +7,8 @@ const gitHash = execSync("git rev-parse --short HEAD").toString().trim();
module.exports = {
all: {
UV_THREADPOOL_SIZE: IS_CI ? NUM_CPUS : 2,
GENERATE_SOURCEMAP: false,
INLINE_RUNTIME_CHUNK: false,
// GENERATE_SOURCEMAP: false,
// INLINE_RUNTIME_CHUNK: false,
DISABLE_ESLINT_PLUGIN: true,
REACT_APP_GIT_HASH: gitHash,
},

View File

@@ -5,10 +5,11 @@ const { createNote, NOTE, getTestId } = require("./utils");
*/
var page = null;
global.page = null;
test.beforeEach(async ({ page: _page }) => {
test.beforeEach(async ({ page: _page, baseURL }) => {
global.page = _page;
page = _page;
await page.goto("http://localhost:3000/");
await page.goto(baseURL);
await page.waitForSelector(getTestId("routeHeader"));
});
test("focus mode", async () => {

View File

@@ -5,6 +5,7 @@ const { getTestId } = require("./utils");
test.beforeEach(async ({ page, baseURL }) => {
await page.goto(baseURL);
await page.waitForSelector(getTestId("routeHeader"));
});
function createRoute(key, header) {

View File

@@ -17,9 +17,10 @@ const { checkNotePresence, isPresent } = require("./utils/conditions");
*/
global.page = null;
test.beforeEach(async ({ page: _page }) => {
test.beforeEach(async ({ page: _page, baseURL }) => {
global.page = _page;
await page.goto("http://localhost:3000/");
await page.goto(baseURL);
await page.waitForSelector(getTestId("routeHeader"));
});
async function fillNotebookDialog(notebook) {

View File

@@ -203,9 +203,10 @@ test.describe("run tests independently", () => {
* @type {Page}
*/
global.page = null;
test.beforeEach(async ({ page: _page }) => {
test.beforeEach(async ({ page: _page, baseURL }) => {
global.page = _page;
await page.goto("http://localhost:3000/");
await page.goto(baseURL);
await page.waitForSelector(getTestId("routeHeader"));
});
test("create a note", async () => {

View File

@@ -15,6 +15,7 @@ test.beforeEach(async ({ page: _page, baseURL }) => {
global.page = _page;
page = _page;
await page.goto(baseURL);
await page.waitForSelector(getTestId("routeHeader"));
});
const USER = {

View File

@@ -14,7 +14,6 @@ async function configureAutoUpdater() {
sendMessageToRenderer(EVENTS.updateAvailable, info);
});
autoUpdater.addListener("download-progress", (progress) => {
console.log("Downloading", progress);
sendMessageToRenderer(EVENTS.updateDownloadProgress, progress);
});
autoUpdater.addListener("update-downloaded", (info) => {

View File

@@ -23,7 +23,7 @@
"emotion-theming": "^10.0.19",
"fast-sort": "^2.1.1",
"file-saver": "^2.0.5",
"framer-motion": "^3.3.0",
"framer-motion": "^4.1.17",
"hash-wasm": "^4.9.0",
"hotkeys-js": "^3.8.3",
"immer": "^9.0.6",
@@ -77,6 +77,7 @@
"patch-package": "^6.4.7",
"source-map-explorer": "^2.5.2",
"typescript": "^4.1.5",
"webpack-bundle-analyzer": "^4.5.0",
"worker-loader": "^3.0.8",
"zx": "^2.0.0"
},

View File

@@ -1,68 +0,0 @@
/* open-sans-regular - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: normal;
font-weight: 400;
src: local(""),
url("/fonts/open-sans-v20-latin-regular.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("/fonts/open-sans-v20-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* open-sans-italic - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: italic;
font-weight: 400;
src: local(""), url("/fonts/open-sans-v20-latin-italic.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("/fonts/open-sans-v20-latin-italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* open-sans-600 - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: normal;
font-weight: 600;
src: local(""), url("/fonts/open-sans-v20-latin-600.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("/fonts/open-sans-v20-latin-600.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* open-sans-600italic - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: italic;
font-weight: 600;
src: local(""),
url("/fonts/open-sans-v20-latin-600italic.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("/fonts/open-sans-v20-latin-600italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* open-sans-700 - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: normal;
font-weight: 700;
src: local(""), url("/fonts/open-sans-v20-latin-700.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("/fonts/open-sans-v20-latin-700.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* open-sans-700italic - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: italic;
font-weight: 700;
src: local(""),
url("/fonts/open-sans-v20-latin-700italic.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("/fonts/open-sans-v20-latin-700italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}

View File

@@ -17,7 +17,7 @@
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAHUUExURUxpcf////////////////////////////////////////////////////////////////////////////////////39/f39/f////////////////////////////////z8/O3s7P7+/vTz8wAAAPz7++/v7/Py8u/u7vDv7+7t7dfX1/79/ezr66Kiovv6+vf29vX09OTj4w0NDfv7++Ti4vr6+vT09Ovq6vn5+f39/fb19dnX1/b29tvb29/f3xMTE/j4+Pn4+O7s7MnJyerq6uno6M/Pz8jIyN3d3dnZ2d7e3kZGRvj39/38/Pf39+zs7PDw8OPi4ubl5cvLy+rp6czMzPLy8vPz887Ozujo6MrKyuDf3+fn59PT0+bm5uXl5c3NzfX19evr6wsLC+/t7ePj4/Ty8u3r6/Lx8eXk5OPh4dDQ0Nza2tjX19vZ2fHw8Nva2tLR0djW1tbW1tra2tLS0uHh4evp6eLi4tXV1djY2L+/vysrK7q6utHR0aGhoa6urrm5uQICAigoKCEhIZCQkFxcXBgYGFRUVBISEm1tbQ4ODklJSbu7uyAgID09PZqamkVFRXh4eLGxsTg4OOTk5FFRUQEBAQoKCnx8fBcXF7/+rP8AAAAfdFJOUwBOqO2h6KT9Mtyr5+w3Str+6TTZOjXb31M24O8BOd0PapSsAAABxElEQVQ4y2NgYOBgYWWXxwLYBdmEGICAjx9Z1NJJVQfB42QE6hdG0RaoaaCqhKSCl4EF1VxNVAXyTAzcaArUUBXwMLBjKDBBViDAII+uwBZFgTy6AjU1awIKNKz1aa0g1VBfB58CDS08CuIb40AKzLAqsJKXN2tXacChwKS+rTtB3tJHRU9ewxmbFZ6tKiqTrSAKdIO1LDEUaPeoqMzsUwAp0NIt8Uj0ijJXQFVgnDJVZcK0XrACXcXY0sjoHM8AfyUkBbOn9KvMmgRSYFEW4hUVqJagHeaShqTAwX7ijJQusAkW5VXRHu6eruFqaigKkqarqAAVKFbYxWiYq1XGedijuMHBWz6xo7m2Rl7RwtvdJaw6NjzCFl2BVVNdshJQgbqBsqtdpIu7nasjXIFXS2cyRL2CW6i6JTCgFCLitUPs4QrM7cDmKZi7FRYVm6o5QeLbDGICLNEqJAVk59rk2djY5Me4FThawQOKFSzrrxmaZWpqBAamQUGmwRaakNTNzMAm7+ekrKmlqOjsrIgAGcAA1ci0NVPiYhAV8zVURgbp6nBg7isuwsDAKKfkp4AEVOHASklKApS9JZmY5bECaS5ZGQYA3vqpy6NoYh0AAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII="
/>
<link
rel="icon"
@@ -25,7 +25,11 @@
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
/>
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
<link
rel="manifest"
href='data:application/manifest+json,{"name":"Notesnook - Your private note taking space","short_name":"Notesnook","icons":[{"src":"favicon.ico","sizes":"64x64 32x32 24x24 16x16","type":"image/x-icon"},{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"start_url":".","theme_color":"rgb(255,255,255)","background_color":"rgb(255,255,255)","display":"standalone"}'
/>
<!-- <link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" /> -->
<link
rel="mask-icon"
href="%PUBLIC_URL%/safari-pinned-tab.svg"
@@ -110,8 +114,73 @@
transform: scale(0.95);
}
}
html,
body {
font-size: 16px;
}
@media only screen and (max-width: 480px) {
html,
body {
font-size: 18px;
background-color: var(--background);
}
}
:root,
body,
#root {
margin: 0 !important;
height: 100%;
overflow: hidden;
}
* {
font-family: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
@keyframes fadeUp {
0% {
transform: translateY(500px);
opacity: 0;
}
80% {
transform: translateY(0px);
opacity: 0.7;
}
100% {
opacity: 1;
}
}
/* svg {
width: 1.5rem;
} */
.ReactModal__Overlay {
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.ReactModal__Overlay--after-open {
opacity: 1;
}
.ReactModal__Overlay--before-close {
opacity: 0;
}
.slide {
background-color: transparent !important;
}
.carousel.carousel-slider {
display: flex;
flex-direction: row;
}
</style>
<link rel="stylesheet" href="/fonts.css" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -8,16 +8,21 @@ import { db } from "./common/db";
import { CHECK_IDS, EV, EVENTS } from "notes-core/common";
import { registerKeyMap } from "./common/key-map";
import { isUserPremium } from "./hooks/use-is-user-premium";
import { loadTrackerScript } from "./utils/analytics";
import Modal from "react-modal";
function AppEffects({ isMobile, isTablet, setShow, slideToIndex }) {
Modal.setAppElement("#root");
if (process.env.NODE_ENV === "production") {
loadTrackerScript();
console.log = () => {};
}
export default function AppEffects({ setShow }) {
const refreshColors = useStore((store) => store.refreshColors);
const refreshMenuPins = useStore((store) => store.refreshMenuPins);
const updateLastSynced = useStore((store) => store.updateLastSynced);
const setProcessingStatus = useStore((store) => store.setProcessingStatus);
const isFocusMode = useStore((store) => store.isFocusMode);
const isEditorOpen = useStore((store) => store.isEditorOpen);
const toggleSideMenu = useStore((store) => store.toggleSideMenu);
const isSideMenuOpen = useStore((store) => store.isSideMenuOpen);
const addReminder = useStore((store) => store.addReminder);
const initUser = useUserStore((store) => store.init);
const initNotes = useNotesStore((store) => store.init);
@@ -115,25 +120,8 @@ function AppEffects({ isMobile, isTablet, setShow, slideToIndex }) {
};
}, []);
useEffect(() => {
if (!isMobile) return;
slideToIndex(isSideMenuOpen ? 0 : 1);
}, [isMobile, slideToIndex, isSideMenuOpen]);
useEffect(() => {
if (!isMobile) return;
slideToIndex(isEditorOpen ? 2 : 1);
}, [isMobile, slideToIndex, isEditorOpen]);
useEffect(() => {
toggleSideMenu(!isMobile);
if (!isMobile && !isTablet && !isFocusMode) setShow(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMobile, isTablet, isFocusMode, toggleSideMenu]);
return <React.Fragment />;
}
export default AppEffects;
function getProcessingStatusFromType(type) {
switch (type) {

View File

@@ -0,0 +1,64 @@
import { useEffect } from "react";
import { useStore } from "./stores/app-store";
import useSlider from "./hooks/use-slider";
import useMobile from "./utils/use-mobile";
import useTablet from "./utils/use-tablet";
export default function MobileAppEffects({ sliderId, overlayId, setShow }) {
const isMobile = useMobile();
const isTablet = useTablet();
const toggleSideMenu = useStore((store) => store.toggleSideMenu);
const setIsEditorOpen = useStore((store) => store.setIsEditorOpen);
const isEditorOpen = useStore((store) => store.isEditorOpen);
const isSideMenuOpen = useStore((store) => store.isSideMenuOpen);
const isFocusMode = useStore((store) => store.isFocusMode);
const [slideToIndex] = useSlider(sliderId, {
onSliding: (e, { lastSlide, position, lastPosition }) => {
if (!isMobile) return;
const offset = 70;
const width = 180;
const percent = offset - (position / width) * offset;
const overlay = document.getElementById("overlay");
if (percent > 0) {
overlay.style.opacity = `${percent}%`;
overlay.style.pointerEvents = "all";
} else {
overlay.style.pointerEvents = "none";
}
},
onChange: (e, { slide, lastSlide }) => {
if (!lastSlide || !isMobile) return;
toggleSideMenu(slide?.index === 0 ? true : false);
setIsEditorOpen(slide?.index === 2 ? true : false);
},
});
useEffect(() => {
if (!isMobile) return;
slideToIndex(isSideMenuOpen ? 0 : 1);
}, [isMobile, slideToIndex, isSideMenuOpen]);
useEffect(() => {
if (!isMobile) return;
slideToIndex(isEditorOpen ? 2 : 1);
}, [isMobile, slideToIndex, isEditorOpen]);
useEffect(() => {
toggleSideMenu(!isMobile);
if (!isMobile && !isTablet && !isFocusMode) setShow(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMobile, isTablet, isFocusMode, toggleSideMenu]);
useEffect(() => {
if (!overlayId) return;
const overlay = document.getElementById(overlayId);
overlay.onclick = () => toggleSideMenu(false);
return () => {
overlay.onclick = null;
};
}, [overlayId, toggleSideMenu]);
return null;
}

View File

@@ -1,3 +1,75 @@
/* open-sans-regular - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: normal;
font-weight: 400;
src: local(""),
url("./assets/fonts/open-sans-v20-latin-regular.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("./assets/fonts/open-sans-v20-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* open-sans-italic - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: italic;
font-weight: 400;
src: local(""),
url("./assets/fonts/open-sans-v20-latin-italic.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("./assets/fonts/open-sans-v20-latin-italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* open-sans-600 - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: normal;
font-weight: 600;
src: local(""),
url("./assets/fonts/open-sans-v20-latin-600.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("./assets/fonts/open-sans-v20-latin-600.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* open-sans-600italic - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: italic;
font-weight: 600;
src: local(""),
url("./assets/fonts/open-sans-v20-latin-600italic.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("./assets/fonts/open-sans-v20-latin-600italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* open-sans-700 - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: normal;
font-weight: 700;
src: local(""),
url("./assets/fonts/open-sans-v20-latin-700.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("./assets/fonts/open-sans-v20-latin-700.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* open-sans-700italic - latin */
@font-face {
font-family: "Open Sans";
font-display: swap;
font-style: italic;
font-weight: 700;
src: local(""),
url("./assets/fonts/open-sans-v20-latin-700italic.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("./assets/fonts/open-sans-v20-latin-700italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
.unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;

View File

@@ -1,81 +1,48 @@
import React, { useState, useEffect } from "react";
import "./app.css";
import { Box, Flex } from "rebass";
import { MotionConfig, AnimationFeature, GesturesFeature } from "framer-motion";
import React, { useState, Suspense } from "react";
import { Box, Flex, Text } from "rebass";
import ThemeProvider from "./components/theme-provider";
import StatusBar from "./components/statusbar";
import Animated from "./components/animated";
import NavigationMenu from "./components/navigationmenu";
import GlobalMenuWrapper from "./components/globalmenuwrapper";
import { NavigationEvents } from "./navigation";
import rootroutes from "./navigation/rootroutes";
import { useStore } from "./stores/app-store";
import { Suspense } from "react";
import { AnimatedFlex } from "./components/animated";
import NavigationMenuPlaceholder from "./components/navigationmenu/index.lite";
import StatusBarPlaceholder from "./components/statusbar/index.lite";
import useMobile from "./utils/use-mobile";
import useTablet from "./utils/use-tablet";
import HashRouter from "./components/hashrouter";
import useSlider from "./hooks/use-slider";
import useRoutes from "./utils/use-routes";
import { clearRouteCache } from "./components/cachedrouter";
import { Loading } from "./components/icons";
import { LazyMotion, domAnimation } from "framer-motion";
import useDatabase from "./hooks/use-database";
import Loader from "./components/loader";
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 CachedRouter = React.lazy(() => import("./components/cached-router"));
const HashRouter = React.lazy(() => import("./components/hash-router"));
const NavigationMenu = React.lazy(() => import("./components/navigation-menu"));
const StatusBar = React.lazy(() => import("./components/status-bar"));
function App() {
const [show, setShow] = useState(true);
const isMobile = useMobile();
const isTablet = useTablet();
const [isAppLoaded, setIsAppLoaded] = useState(false);
const toggleSideMenu = useStore((store) => store.toggleSideMenu);
const setIsEditorOpen = useStore((store) => store.setIsEditorOpen);
const [sliderRef, slideToIndex] = useSlider({
onSliding: (e, { lastSlide, position, lastPosition }) => {
if (!isMobile) return;
const offset = 70;
const width = 180;
const percent = offset - (position / width) * offset;
const overlay = document.getElementById("overlay");
if (percent > 0) {
overlay.style.opacity = `${percent}%`;
overlay.style.pointerEvents = "all";
} else {
overlay.style.pointerEvents = "none";
}
},
onChange: (e, { slide, lastSlide }) => {
if (!lastSlide || !isMobile) return;
toggleSideMenu(slide?.index === 0 ? true : false);
setIsEditorOpen(slide?.index === 2 ? true : false);
},
});
useEffect(() => {
function onNavigate() {
NavigationEvents.unsubscribe("onNavigate", onNavigate);
setIsAppLoaded(true);
}
NavigationEvents.subscribe("onNavigate", onNavigate);
return () => {
NavigationEvents.unsubscribe("onNavigate", onNavigate);
};
}, []);
const [isAppLoaded] = useDatabase();
return (
<MotionConfig features={[AnimationFeature, GesturesFeature]}>
<LazyMotion features={domAnimation} strict>
<ThemeProvider>
{isAppLoaded && (
<Suspense fallback={<div style={{ display: "none" }} />}>
<AppEffects
slideToIndex={slideToIndex}
setShow={setShow}
isMobile={isMobile}
isTablet={isTablet}
/>
<GlobalMenuWrapper />
<AppEffects setShow={setShow} />
{isMobile && (
<MobileAppEffects
sliderId="slider"
overlayId="overlay"
setShow={setShow}
/>
)}
</Suspense>
)}
<GlobalMenuWrapper />
<Flex
flexDirection="column"
id="app"
@@ -84,7 +51,7 @@ function App() {
sx={{ overflow: "hidden" }}
>
<Flex
ref={sliderRef}
id="slider"
variant="rowFill"
overflowX={["auto", "hidden"]}
sx={{
@@ -99,13 +66,18 @@ function App() {
scrollSnapAlign: "start",
}}
>
<NavigationMenu
toggleNavigationContainer={(state) => {
if (!isMobile) setShow(state || !show);
<SuspenseLoader
condition={isAppLoaded}
component={NavigationMenu}
props={{
toggleNavigationContainer: (state) => {
if (!isMobile) setShow(state || !show);
},
}}
fallback={<NavigationMenuPlaceholder />}
/>
</Flex>
<Animated.Flex
<AnimatedFlex
className="listMenu"
variant="columnFill"
initial={{
@@ -134,9 +106,16 @@ function App() {
}}
flexShrink={0}
>
<Suspense fallback={<div />}>
<CachedRouter />
</Suspense>
<SuspenseLoader
condition={isAppLoaded}
component={CachedRouter}
fallback={
<Loader
title="Did you know?"
text="All your notes are encrypted on your device."
/>
}
/>
{isMobile && (
<Box
id="overlay"
@@ -152,12 +131,9 @@ function App() {
pointerEvents: "none",
}}
bg="black"
onClick={() => {
toggleSideMenu(false);
}}
/>
)}
</Animated.Flex>
</AnimatedFlex>
<Flex
width={["100vw", "100%"]}
flexShrink={[0, 1]}
@@ -166,20 +142,37 @@ function App() {
}}
flexDirection="column"
>
<HashRouter />
<SuspenseLoader
fallback={
<Loader
title="Fun fact"
text="Notesnook was released in January 2021 by a team of only 3 people."
/>
}
component={HashRouter}
condition={isAppLoaded}
/>
</Flex>
</Flex>
<StatusBar />
<SuspenseLoader
fallback={<StatusBarPlaceholder />}
component={StatusBar}
condition={isAppLoaded}
/>
</Flex>
</ThemeProvider>
</MotionConfig>
</LazyMotion>
);
}
function Root() {
const route = useRoutes(rootroutes);
if (route) clearRouteCache();
return route?.component || <App />;
}
export default App;
export default Root;
function SuspenseLoader({ condition, props, component: Component, fallback }) {
if (!condition) return fallback;
return (
<Suspense fallback={fallback}>
<Component {...props} />
</Suspense>
);
}

View File

@@ -1,11 +1,3 @@
import StorageInterface from "../interfaces/storage";
import FS from "../interfaces/fs";
import Config from "../utils/config";
import http from "notes-core/utils/http";
import { EV, EVENTS } from "notes-core/common";
import { getCurrentHash, hashNavigate } from "../navigation";
import { isTesting } from "../utils/platform";
global.HTMLParser = new DOMParser().parseFromString(
"<body></body>",
"text/html"
@@ -16,67 +8,31 @@ global.HTMLParser = new DOMParser().parseFromString(
var db;
async function initializeDatabase() {
const { default: Database } = await import("notes-core/api");
db = new Database(StorageInterface, EventSource, FS);
const { default: Storage } = await import("../interfaces/storage");
const { default: FS } = await import("../interfaces/fs");
db = new Database(Storage, EventSource, FS);
if (isTesting()) {
db.host({
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
});
} else {
db.host({
API_HOST: "http://localhost:5264",
AUTH_HOST: "http://localhost:8264",
SSE_HOST: "http://localhost:7264",
});
// db.host({
// API_HOST: "http://192.168.10.29:5264",
// AUTH_HOST: "http://192.168.10.29:8264",
// SSE_HOST: "http://192.168.10.29:7264",
// });
}
// if (isTesting()) {
// db.host({
// API_HOST: "https://api.notesnook.com",
// AUTH_HOST: "https://auth.streetwriters.co",
// SSE_HOST: "https://events.streetwriters.co",
// });
// } else {
// db.host({
// API_HOST: "http://localhost:5264",
// AUTH_HOST: "http://localhost:8264",
// SSE_HOST: "http://localhost:7264",
// });
// // db.host({
// // API_HOST: "http://192.168.10.29:5264",
// // AUTH_HOST: "http://192.168.10.29:8264",
// // SSE_HOST: "http://192.168.10.29:7264",
// // });
// }
await db.init();
if (!isAppHydrated() && !isTesting()) {
try {
loadDefaultNotes(db);
} catch (e) {}
}
return db;
}
export { db, initializeDatabase };
function isAppHydrated() {
return Config.get("hydrated", false);
}
function setAppHydrated() {
return Config.set("hydrated", true);
}
async function loadDefaultNotes(db) {
const notes = await http.get("/notes/index_v14.json");
if (!notes) return;
let autoOpenId;
const hash = getCurrentHash().replaceAll("#", "");
for (let note of notes) {
const content = await http.get(note.webContent);
let id = await db.notes.add({
title: note.title,
headline: note.headline,
localOnly: true,
content: { type: "tiny", data: content },
});
if (note.autoOpen) autoOpenId = id;
}
if (autoOpenId) {
hashNavigate(`/notes/${autoOpenId}/edit`);
if (hash) setTimeout(() => hashNavigate(hash), 100);
}
setAppHydrated();
EV.publish(EVENTS.appRefreshRequested);
}

View File

@@ -1,6 +1,6 @@
const SINGLE_LINE_HEIGHT = 1.4;
const DEFAULT_FONT_SIZE = document.getElementById("p").clientHeight - 1;
console.log("HEIGHT", DEFAULT_FONT_SIZE);
const MAX_HEIGHTS = {
note: SINGLE_LINE_HEIGHT * 7 * DEFAULT_FONT_SIZE,
notebook: SINGLE_LINE_HEIGHT * 7 * DEFAULT_FONT_SIZE,

View File

@@ -1,12 +1,9 @@
import { m as motion } from "framer-motion";
import { m } from "framer-motion";
import { Flex, Box, Image, Text } from "rebass";
import { Input } from "@rebass/forms";
const Animated = {
Flex: motion.custom(Flex),
Box: motion.custom(Box),
Image: motion.custom(Image),
Text: motion.custom(Text),
Input: motion.custom(Input),
};
export default Animated;
export const AnimatedFlex = m(Flex);
export const AnimatedBox = m(Box);
export const AnimatedImage = m(Image);
export const AnimatedText = m(Text);
export const AnimatedInput = m(Input);

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import { NavigationEvents } from "../../navigation";
import { getHomeRoute, NavigationEvents } from "../../navigation";
import useRoutes from "../../utils/use-routes";
import RouteContainer from "../route-container";
import ThemeProvider from "../theme-provider";
@@ -8,10 +8,12 @@ import routes from "../../navigation/routes";
var cache = {};
function CachedRouter() {
const RouteResult = useRoutes(routes, { fallbackRoute: "/" });
const [RouteResult, location] = useRoutes(routes, {
fallbackRoute: getHomeRoute(),
});
useEffect(() => {
if (!RouteResult) return;
NavigationEvents.publish("onNavigate", RouteResult);
NavigationEvents.publish("onNavigate", RouteResult, location);
window.currentViewType = RouteResult.type;
window.currentViewKey = RouteResult.key;
@@ -45,7 +47,7 @@ function CachedRouter() {
route
);
}
}, [RouteResult]);
}, [RouteResult, location]);
return (
<RouteContainer

View File

@@ -4,13 +4,6 @@ import Dialog from "./dialog";
import * as Icon from "../icons";
import { useStore as useUserStore } from "../../stores/user-store";
import { getCouponData, upgrade } from "../../common/checkout";
import { ReactComponent as Personalization } from "../../assets/accent.svg";
import { ReactComponent as Backups } from "../../assets/backup.svg";
import { ReactComponent as Export } from "../../assets/export.svg";
import { ReactComponent as Organize } from "../../assets/organize.svg";
import { ReactComponent as RichText } from "../../assets/richtext.svg";
import { ReactComponent as Sync } from "../../assets/sync.svg";
import { ReactComponent as Vault } from "../../assets/vault.svg";
import getSymbolFromCurrency from "currency-symbol-map";
import { ANALYTICS_EVENTS, trackEvent } from "../../utils/analytics";
import { navigate } from "../../navigation";
@@ -19,77 +12,41 @@ const premiumDetails = [
{
title: "Unlimited attachments",
description: "Your notes will be automatically synced to all your devices.",
illustration: {
icon: Sync,
width: "40%",
},
},
{
title: "Unlimited storage",
description: "Your notes will be automatically synced to all your devices.",
illustration: {
icon: Sync,
width: "40%",
},
},
{
title: "Unlimited notebooks & tags",
description:
"Make unlimited notebooks and tags, and assign colors to your notes for quick access.",
illustration: {
icon: Organize,
width: "40%",
},
},
{
title: "Automatic syncing",
description: "Your notes will be automatically synced to all your devices.",
illustration: {
icon: Sync,
width: "40%",
},
},
{
title: "Secure vault for notes",
description:
"Lock any note with a password and keep sensitive data under lock and key.",
illustration: {
icon: Vault,
width: "35%",
},
},
{
title: "Full rich text editor + markdown support",
description:
"Add images, links, tables and lists to your notes, and use markdown for fast editing.",
illustration: {
icon: RichText,
width: "50%",
},
},
{
title: "Multi-format exports",
description: "Export your notes in PDF, Markdown, or HTML formats.",
illustration: {
icon: Export,
width: "25%",
},
},
{
title: "Automatic encrypted backups",
description: "Enable daily or weekly backups with automatic encryption.",
illustration: {
icon: Backups,
width: "25%",
},
},
{
title: "Customize Notesnook",
description: "Change app colors and turn on automatic theme switching.",
illustration: {
icon: Personalization,
width: "50%",
},
},
{
title: (
@@ -100,10 +57,6 @@ const premiumDetails = [
),
description:
"Pro users get access to special channels and priority support on our Discord server.",
illustration: {
icon: Personalization,
width: "50%",
},
},
];

View File

@@ -7,7 +7,7 @@ import {
store as editorstore,
} from "../../stores/editor-store";
import { useStore as useAppStore } from "../../stores/app-store";
import Animated from "../animated";
import { AnimatedFlex } from "../animated";
import Header from "./header";
import useMobile from "../../utils/use-mobile";
import useTablet from "../../utils/use-tablet";
@@ -126,7 +126,7 @@ function Editor({ noteId, nonce }) {
alignItems: "center",
}}
/>
<Animated.Flex
<AnimatedFlex
variant="columnFill"
className="editor"
sx={{
@@ -179,7 +179,7 @@ function Editor({ noteId, nonce }) {
</>
) : null}
</Suspense>
</Animated.Flex>
</AnimatedFlex>
</Flex>
<Properties noteId={noteId} />
</Flex>

View File

@@ -6,7 +6,7 @@ import { useStore as useAppStore } from "../../stores/app-store";
import { useStore as useThemeStore } from "../../stores/theme-store";
import { useStore, store } from "../../stores/editor-store";
import { showToast } from "../../utils/toast";
import Animated from "../animated";
import { AnimatedInput } from "../animated";
import { showPublishView } from "../publish-view";
import { db } from "../../common/db";
@@ -163,7 +163,7 @@ function Toolbar(props) {
clearSession();
}}
/>
<Animated.Input
<AnimatedInput
ml={[2, 2, 0]}
initial={{
opacity: isTitleVisible ? 1 : 0,

View File

@@ -1,7 +1,7 @@
import * as Icon from "../icons";
import React, { useMemo } from "react";
import { Button, Flex, Text } from "rebass";
import Animated from "../animated";
import { AnimatedFlex } from "../animated";
import { db } from "../../common/db";
import { useOpenContextMenu } from "../../utils/useContextMenu";
import { useStore as useNoteStore } from "../../stores/note-store";
@@ -99,7 +99,7 @@ function GroupHeader(props) {
if (!title) return null;
return (
<Animated.Flex
<AnimatedFlex
transition={{ duration: 0.3, repeatType: "reverse", repeat: 3 }}
onClick={(e) => {
if (groups.length <= 0) return;
@@ -165,7 +165,7 @@ function GroupHeader(props) {
)}
</Flex>
)}
</Animated.Flex>
</AnimatedFlex>
);
}
export default GroupHeader;

View File

@@ -1,8 +1,101 @@
import React, { useState } from "react";
import MDIIcon from "@mdi/react";
import * as Icons from "@mdi/js";
import {
mdiPlus,
mdiHomeVariantOutline,
mdiMinus,
mdiBookOutline,
mdiNotebookOutline,
mdiArrowLeft,
mdiArrowRight,
mdiArrowDown,
mdiBookPlusMultipleOutline,
mdiBookmarkOutline,
mdiAlert,
mdiShieldOutline,
mdiLockOpenOutline,
mdiLockOutline,
mdiStar,
mdiStarOutline,
mdiCircle,
mdiCircleOutline,
mdiUpdate,
mdiCheck,
mdiClose,
mdiDotsVertical,
mdiTrashCanOutline,
mdiBookRemoveOutline,
mdiMagnify,
mdiMenu,
mdiLoginVariant,
mdiEmailAlertOutline,
mdiAccountOutline,
mdiLogoutVariant,
mdiSunglasses,
mdiGlasses,
mdiCogOutline,
mdiHomeOutline,
mdiRecycle,
mdiSync,
mdiLoading,
mdiExportVariant,
mdiArrowExpandDown,
mdiArrowTopRightThick,
mdiChevronLeft,
mdiChevronRight,
mdiPound,
mdiPinOutline,
mdiPin,
mdiWeatherNight,
mdiWeatherSunny,
mdiThemeLightDark,
mdiCheckCircle,
mdiLanguageMarkdownOutline,
mdiFilePdfOutline,
mdiLanguageHtml5,
mdiFormatTitle,
mdiAlertCircle,
mdiInformation,
mdiToggleSwitchOffOutline,
mdiToggleSwitchOutline,
mdiBackupRestore,
mdiCurrencyUsdCircleOutline,
mdiPencil,
mdiUndoVariant,
mdiRedoVariant,
mdiTune,
mdiChevronDown,
mdiChevronUp,
mdiSortAscending,
mdiSortDescending,
mdiEye,
mdiEyeOff,
mdiFullscreen,
mdiFullscreenExit,
mdiBullhorn,
mdiCloudUploadOutline,
mdiCloudCheckOutline,
mdiContentCopy,
mdiCheckboxMultipleMarkedCircleOutline,
mdiBookEditOutline,
mdiDeleteForeverOutline,
mdiTextBoxMultipleOutline,
mdiRocketLaunchOutline,
mdiShareVariantOutline,
mdiFormTextboxPassword,
mdiBomb,
mdiViewHeadline,
mdiViewSequentialOutline,
mdiEmailCheckOutline,
mdiDiscord,
mdiTwitter,
mdiReddit,
mdiFileOutline,
mdiImage,
mdiNoteOutline,
} from "@mdi/js";
import { useTheme } from "emotion-theming";
import Animated from "../animated";
import { AnimatedFlex } from "../animated";
function Icon({ title, name, size = 24, color = "icon", stroke, rotate }) {
const theme = useTheme();
@@ -25,7 +118,7 @@ function createIcon(name, rotate = false) {
return function (props) {
const [isHovering, setIsHovering] = useState();
return (
<Animated.Flex
<AnimatedFlex
id={props.id}
title={props.title}
variant={props.variant}
@@ -49,138 +142,109 @@ function createIcon(name, rotate = false) {
}
{...props}
/>
</Animated.Flex>
</AnimatedFlex>
);
};
}
export const Plus = createIcon(Icons.mdiPlus);
export const Note = createIcon(Icons.mdiHomeVariantOutline);
export const Minus = createIcon(Icons.mdiMinus);
export const Notebook = createIcon(Icons.mdiBookOutline);
export const Notebook2 = createIcon(Icons.mdiNotebookOutline);
export const ArrowLeft = createIcon(Icons.mdiArrowLeft);
export const ArrowRight = createIcon(Icons.mdiArrowRight);
export const ArrowDown = createIcon(Icons.mdiArrowDown);
export const Move = createIcon(Icons.mdiBookPlusMultipleOutline);
export const Topic = createIcon(Icons.mdiBookmarkOutline);
export const Alert = createIcon(Icons.mdiAlert);
export const Vault = createIcon(Icons.mdiShieldOutline);
export const Unlock = createIcon(Icons.mdiLockOpenOutline);
export const Lock = createIcon(Icons.mdiLockOutline);
export const Star = createIcon(Icons.mdiStar);
export const StarOutline = createIcon(Icons.mdiStarOutline);
export const Circle = createIcon(Icons.mdiCircle);
export const CircleEmpty = createIcon(Icons.mdiCircleOutline);
export const Update = createIcon(Icons.mdiUpdate);
export const Check = createIcon(Icons.mdiCheck);
export const Cross = createIcon(Icons.mdiClose);
export const MoreVertical = createIcon(Icons.mdiDotsVertical);
export const Trash = createIcon(Icons.mdiTrashCanOutline);
export const TopicRemove = createIcon(Icons.mdiBookRemoveOutline);
export const Search = createIcon(Icons.mdiMagnify);
export const Menu = createIcon(Icons.mdiMenu);
export const Login = createIcon(Icons.mdiLoginVariant);
export const Email = createIcon(Icons.mdiEmailAlertOutline);
export const Signup = createIcon(Icons.mdiAccountOutline);
export const Logout = createIcon(Icons.mdiLogoutVariant);
export const FocusMode = createIcon(Icons.mdiSunglasses);
export const NormalMode = createIcon(Icons.mdiGlasses);
export const Settings = createIcon(Icons.mdiCogOutline);
export const Home = createIcon(Icons.mdiHomeOutline);
export const Restore = createIcon(Icons.mdiRecycle);
export const Sync = createIcon(Icons.mdiSync);
export const Loading = createIcon(Icons.mdiLoading, true);
export const Export = createIcon(Icons.mdiExportVariant);
export const AddToNotebook = createIcon(Icons.mdiBookPlusMultipleOutline);
export const Expand = createIcon(Icons.mdiArrowExpandDown);
export const Shortcut = createIcon(Icons.mdiArrowTopRightThick);
/** Properties Icons */
export const ChevronLeft = createIcon(Icons.mdiChevronLeft);
export const ChevronRight = createIcon(Icons.mdiChevronRight);
export const Close = createIcon(Icons.mdiClose);
export const Tag = createIcon(Icons.mdiPound);
export const Tag2 = createIcon(Icons.mdiPound);
export const Pin = createIcon(Icons.mdiPinOutline);
export const PinFilled = createIcon(Icons.mdiPin);
/** Settings Icons */
export const User = createIcon(Icons.mdiAccountOutline);
export const DarkMode = createIcon(Icons.mdiWeatherNight);
export const LightMode = createIcon(Icons.mdiWeatherSunny);
export const Theme = createIcon(Icons.mdiThemeLightDark);
export const Checkmark = createIcon(Icons.mdiCheck);
export const CheckCircle = createIcon(Icons.mdiCheckCircle);
export const Properties = createIcon(Icons.mdiDotsVertical);
// FORMATS
export const Markdown = createIcon(Icons.mdiLanguageMarkdownOutline);
export const PDF = createIcon(Icons.mdiFilePdfOutline);
export const HTML = createIcon(Icons.mdiLanguageHtml5);
export const Text = createIcon(Icons.mdiFormatTitle);
// TOAST
export const Success = createIcon(Icons.mdiCheckCircle);
export const Error = createIcon(Icons.mdiAlertCircle);
export const Warn = createIcon(Icons.mdiAlert);
export const Info = createIcon(Icons.mdiInformation);
export const ToggleUnchecked = createIcon(Icons.mdiToggleSwitchOffOutline);
export const ToggleChecked = createIcon(Icons.mdiToggleSwitchOutline);
export const Backup = createIcon(Icons.mdiBackupRestore);
export const Buy = createIcon(Icons.mdiCurrencyUsdCircleOutline);
export const Edit = createIcon(Icons.mdiPencil);
export const Undo = createIcon(Icons.mdiUndoVariant);
export const Redo = createIcon(Icons.mdiRedoVariant);
export const Filter = createIcon(Icons.mdiTune);
export const ChevronDown = createIcon(Icons.mdiChevronDown);
export const ChevronUp = createIcon(Icons.mdiChevronUp);
export const SortAsc = createIcon(Icons.mdiSortAscending);
export const SortDesc = createIcon(Icons.mdiSortDescending);
export const PasswordInvisible = createIcon(Icons.mdiEye);
export const PasswordVisible = createIcon(Icons.mdiEyeOff);
export const Fullscreen = createIcon(Icons.mdiFullscreen);
export const ExitFullscreen = createIcon(Icons.mdiFullscreenExit);
export const Announcement = createIcon(Icons.mdiBullhorn);
export const Publish = createIcon(Icons.mdiCloudUploadOutline);
export const Published = createIcon(Icons.mdiCloudCheckOutline);
export const Copy = createIcon(Icons.mdiContentCopy);
export const Select = createIcon(Icons.mdiCheckboxMultipleMarkedCircleOutline);
export const NotebookEdit = createIcon(Icons.mdiBookEditOutline);
export const DeleteForver = createIcon(Icons.mdiDeleteForeverOutline);
export const Monographs = createIcon(Icons.mdiTextBoxMultipleOutline);
export const Rocket = createIcon(Icons.mdiRocketLaunchOutline);
export const Share = createIcon(Icons.mdiShareVariantOutline);
export const Password = createIcon(Icons.mdiFormTextboxPassword);
export const Destruct = createIcon(Icons.mdiBomb);
export const CompactView = createIcon(Icons.mdiViewHeadline);
export const DetailedView = createIcon(Icons.mdiViewSequentialOutline);
export const MailCheck = createIcon(Icons.mdiEmailCheckOutline);
export const Discord = createIcon(Icons.mdiDiscord);
export const Twitter = createIcon(Icons.mdiTwitter);
export const Reddit = createIcon(Icons.mdiReddit);
export const Dismiss = createIcon(Icons.mdiClose);
export const File = createIcon(Icons.mdiFileOutline);
export const Download = createIcon(Icons.mdiArrowDown);
export const ImageDownload = createIcon(Icons.mdiImage);
export const Plus = createIcon(mdiPlus);
export const Note = createIcon(mdiNoteOutline);
export const Minus = createIcon(mdiMinus);
export const Notebook = createIcon(mdiBookOutline);
export const Notebook2 = createIcon(mdiNotebookOutline);
export const ArrowLeft = createIcon(mdiArrowLeft);
export const ArrowRight = createIcon(mdiArrowRight);
export const ArrowDown = createIcon(mdiArrowDown);
export const Move = createIcon(mdiBookPlusMultipleOutline);
export const Topic = createIcon(mdiBookmarkOutline);
export const Alert = createIcon(mdiAlert);
export const Vault = createIcon(mdiShieldOutline);
export const Unlock = createIcon(mdiLockOpenOutline);
export const Lock = createIcon(mdiLockOutline);
export const Star = createIcon(mdiStar);
export const StarOutline = createIcon(mdiStarOutline);
export const Circle = createIcon(mdiCircle);
export const CircleEmpty = createIcon(mdiCircleOutline);
export const Update = createIcon(mdiUpdate);
export const Check = createIcon(mdiCheck);
export const Cross = createIcon(mdiClose);
export const MoreVertical = createIcon(mdiDotsVertical);
export const Trash = createIcon(mdiTrashCanOutline);
export const TopicRemove = createIcon(mdiBookRemoveOutline);
export const Search = createIcon(mdiMagnify);
export const Menu = createIcon(mdiMenu);
export const Login = createIcon(mdiLoginVariant);
export const Email = createIcon(mdiEmailAlertOutline);
export const Signup = createIcon(mdiAccountOutline);
export const Logout = createIcon(mdiLogoutVariant);
export const FocusMode = createIcon(mdiSunglasses);
export const NormalMode = createIcon(mdiGlasses);
export const Settings = createIcon(mdiCogOutline);
export const Home = createIcon(mdiHomeOutline);
export const Restore = createIcon(mdiRecycle);
export const Sync = createIcon(mdiSync);
export const Loading = createIcon(mdiLoading, true);
export const Export = createIcon(mdiExportVariant);
export const AddToNotebook = createIcon(mdiBookPlusMultipleOutline);
export const Expand = createIcon(mdiArrowExpandDown);
export const Shortcut = createIcon(mdiArrowTopRightThick);
export const ChevronLeft = createIcon(mdiChevronLeft);
export const ChevronRight = createIcon(mdiChevronRight);
export const Close = createIcon(mdiClose);
export const Tag = createIcon(mdiPound);
export const Tag2 = createIcon(mdiPound);
export const Pin = createIcon(mdiPinOutline);
export const PinFilled = createIcon(mdiPin);
export const User = createIcon(mdiAccountOutline);
export const DarkMode = createIcon(mdiWeatherNight);
export const LightMode = createIcon(mdiWeatherSunny);
export const Theme = createIcon(mdiThemeLightDark);
export const Checkmark = createIcon(mdiCheck);
export const CheckCircle = createIcon(mdiCheckCircle);
export const Properties = createIcon(mdiDotsVertical);
export const Markdown = createIcon(mdiLanguageMarkdownOutline);
export const PDF = createIcon(mdiFilePdfOutline);
export const HTML = createIcon(mdiLanguageHtml5);
export const Text = createIcon(mdiFormatTitle);
export const Success = createIcon(mdiCheckCircle);
export const Error = createIcon(mdiAlertCircle);
export const Warn = createIcon(mdiAlert);
export const Info = createIcon(mdiInformation);
export const ToggleUnchecked = createIcon(mdiToggleSwitchOffOutline);
export const ToggleChecked = createIcon(mdiToggleSwitchOutline);
export const Backup = createIcon(mdiBackupRestore);
export const Buy = createIcon(mdiCurrencyUsdCircleOutline);
export const Edit = createIcon(mdiPencil);
export const Undo = createIcon(mdiUndoVariant);
export const Redo = createIcon(mdiRedoVariant);
export const Filter = createIcon(mdiTune);
export const ChevronDown = createIcon(mdiChevronDown);
export const ChevronUp = createIcon(mdiChevronUp);
export const SortAsc = createIcon(mdiSortAscending);
export const SortDesc = createIcon(mdiSortDescending);
export const PasswordInvisible = createIcon(mdiEye);
export const PasswordVisible = createIcon(mdiEyeOff);
export const Fullscreen = createIcon(mdiFullscreen);
export const ExitFullscreen = createIcon(mdiFullscreenExit);
export const Announcement = createIcon(mdiBullhorn);
export const Publish = createIcon(mdiCloudUploadOutline);
export const Published = createIcon(mdiCloudCheckOutline);
export const Copy = createIcon(mdiContentCopy);
export const Select = createIcon(mdiCheckboxMultipleMarkedCircleOutline);
export const NotebookEdit = createIcon(mdiBookEditOutline);
export const DeleteForver = createIcon(mdiDeleteForeverOutline);
export const Monographs = createIcon(mdiTextBoxMultipleOutline);
export const Rocket = createIcon(mdiRocketLaunchOutline);
export const Share = createIcon(mdiShareVariantOutline);
export const Password = createIcon(mdiFormTextboxPassword);
export const Destruct = createIcon(mdiBomb);
export const CompactView = createIcon(mdiViewHeadline);
export const DetailedView = createIcon(mdiViewSequentialOutline);
export const MailCheck = createIcon(mdiEmailCheckOutline);
export const Discord = createIcon(mdiDiscord);
export const Twitter = createIcon(mdiTwitter);
export const Reddit = createIcon(mdiReddit);
export const Dismiss = createIcon(mdiClose);
export const File = createIcon(mdiFileOutline);
export const Download = createIcon(mdiArrowDown);
export const ImageDownload = createIcon(mdiImage);

View File

@@ -0,0 +1,21 @@
import { Flex, Text } from "rebass";
import { Loading } from "../icons";
export default function Loader({ title, text }) {
return (
<Flex
flexDirection="column"
flex={1}
justifyContent="center"
alignItems="center"
>
<Loading rotate />
<Text variant="subtitle" mt={4}>
{title}
</Text>
<Text variant="body" mt={2}>
{text}
</Text>
</Flex>
);
}

View File

@@ -4,7 +4,7 @@ import React, { useEffect, useMemo } from "react";
import { Flex, Box, Text, Button } from "rebass";
import { useIsUserPremium } from "../../hooks/use-is-user-premium";
import useMobile from "../../utils/use-mobile";
import Animated from "../animated";
import { AnimatedFlex } from "../animated";
function Menu(props) {
const { menuItems, data, closeMenu, id, style, sx, state } = props;
@@ -193,7 +193,7 @@ function MobileMenuContainer({ style, id, state, title, children }) {
overflow="hidden"
sx={{ position: "relative" }}
>
<Animated.Flex
<AnimatedFlex
width="100%"
bg="background"
sx={{
@@ -221,7 +221,7 @@ function MobileMenuContainer({ style, id, state, title, children }) {
</Text>
{children}
</Flex>
</Animated.Flex>
</AnimatedFlex>
</Flex>
);
}

View File

@@ -1,16 +1,32 @@
import React, { useCallback } from "react";
import { useCallback } from "react";
import { Box, Flex } from "rebass";
import { useStore as useAppStore } from "../../stores/app-store";
import * as Icon from "../icons";
import { useStore as useUserStore } from "../../stores/user-store";
import { useStore as useThemeStore } from "../../stores/theme-store";
import Animated from "../animated";
import {
Note,
Notebook,
StarOutline,
Monographs,
Tag,
Trash,
Settings,
Notebook2,
Tag2,
Topic,
DarkMode,
LightMode,
Sync,
Login,
Circle,
} from "../icons";
import { AnimatedFlex } from "../animated";
import NavigationItem from "./navigation-item";
import { navigate } from "../../navigation";
import { db } from "../../common/db";
import useMobile from "../../utils/use-mobile";
import { useLocation } from "wouter";
import { showRenameColorDialog } from "../../common/dialog-controller";
import { useStore as useAppStore } from "../../stores/app-store";
import { useStore as useUserStore } from "../../stores/user-store";
import { useStore as useThemeStore } from "../../stores/theme-store";
import useLocation from "../../hooks/use-location";
function shouldSelectNavItem(route, pin) {
if (pin.type === "notebook") {
@@ -23,31 +39,31 @@ function shouldSelectNavItem(route, pin) {
}
const routes = [
{ title: "Notes", path: "/", icon: Icon.Note },
{ title: "Notes", path: "/notes", icon: Note },
{
title: "Notebooks",
path: "/notebooks",
icon: Icon.Notebook,
icon: Notebook,
},
{
title: "Favorites",
path: "/favorites",
icon: Icon.StarOutline,
icon: StarOutline,
},
{ title: "Tags", path: "/tags", icon: Icon.Tag },
{ title: "Tags", path: "/tags", icon: Tag },
{
title: "Monographs",
path: "/monographs",
icon: Icon.Monographs,
icon: Monographs,
},
{ title: "Trash", path: "/trash", icon: Icon.Trash },
{ title: "Trash", path: "/trash", icon: Trash },
];
const bottomRoutes = [
{
title: "Settings",
path: "/settings",
icon: Icon.Settings,
icon: Settings,
},
];
@@ -77,7 +93,7 @@ function NavigationMenu(props) {
);
return (
<Animated.Flex
<AnimatedFlex
id="navigationmenu"
flexDirection="column"
justifyContent="space-between"
@@ -141,7 +157,7 @@ function NavigationMenu(props) {
<NavigationItem
key={color.id}
title={db.colors.alias(color.id)}
icon={Icon.Circle}
icon={Circle}
selected={location === `/colors/${color.id}`}
color={color.title.toLowerCase()}
onClick={() => {
@@ -181,10 +197,10 @@ function NavigationMenu(props) {
}}
icon={
pin.type === "notebook"
? Icon.Notebook2
? Notebook2
: pin.type === "tag"
? Icon.Tag2
: Icon.Topic
? Tag2
: Topic
}
isShortcut
selected={shouldSelectNavItem(location, pin)}
@@ -204,13 +220,13 @@ function NavigationMenu(props) {
{theme === "light" ? (
<NavigationItem
title="Dark mode"
icon={Icon.DarkMode}
icon={DarkMode}
onClick={toggleNightMode}
/>
) : (
<NavigationItem
title="Light mode"
icon={Icon.LightMode}
icon={LightMode}
onClick={toggleNightMode}
/>
)}
@@ -219,14 +235,14 @@ function NavigationMenu(props) {
<NavigationItem
title="Sync"
isLoading={isSyncing}
icon={Icon.Sync}
icon={Sync}
onClick={sync}
/>
</>
) : (
<NavigationItem
title="Login"
icon={Icon.Login}
icon={Login}
onClick={() => navigate("/login")}
/>
)}
@@ -242,7 +258,7 @@ function NavigationMenu(props) {
/>
))}
</Flex>
</Animated.Flex>
</AnimatedFlex>
);
}
export default NavigationMenu;

View File

@@ -0,0 +1,190 @@
import { Flex } from "rebass";
import { useStore as useThemeStore } from "../../stores/theme-store";
import { Button, Text } from "rebass";
import useTablet from "../../utils/use-tablet";
import {
Note,
Notebook,
StarOutline,
Monographs,
Tag,
Trash,
Settings,
DarkMode,
LightMode,
Sync,
Login,
} from "../icons";
import useLocation from "../../hooks/use-location";
const routes = [
{ title: "Notes", path: "/", icon: Note },
{
title: "Notebooks",
path: "/notebooks",
icon: Notebook,
},
{
title: "Favorites",
path: "/favorites",
icon: StarOutline,
},
{ title: "Tags", path: "/tags", icon: Tag },
{
title: "Monographs",
path: "/monographs",
icon: Monographs,
},
{ title: "Trash", path: "/trash", icon: Trash },
];
const bottomRoutes = [
{
title: "Settings",
path: "/settings",
icon: Settings,
},
];
const NAVIGATION_MENU_WIDTH = "10em";
const NAVIGATION_MENU_TABLET_WIDTH = "4em";
function NavigationMenu() {
const isLoggedIn = false;
const theme = useThemeStore((store) => store.theme);
const toggleNightMode = useThemeStore((store) => store.toggleNightMode);
const [location] = useLocation();
return (
<Flex
id="navigationmenu"
flexDirection="column"
justifyContent="space-between"
flex={1}
sx={{
borderRight: "1px solid",
borderRightColor: "border",
minWidth: [
NAVIGATION_MENU_WIDTH,
NAVIGATION_MENU_TABLET_WIDTH,
NAVIGATION_MENU_WIDTH,
],
maxWidth: [
NAVIGATION_MENU_WIDTH,
NAVIGATION_MENU_TABLET_WIDTH,
NAVIGATION_MENU_WIDTH,
],
zIndex: 1,
height: "auto",
position: "relative",
}}
bg={"bgSecondary"}
px={0}
>
<Flex
flexDirection="column"
sx={{
overflow: "scroll",
scrollbarWidth: "none",
"::-webkit-scrollbar": { width: 0, height: 0 },
msOverflowStyle: "none",
}}
>
{routes.map((item) => (
<NavigationItem
key={item.path}
title={item.title}
icon={item.icon}
selected={
item.path === "/"
? location === item.path
: location.startsWith(item.path)
}
/>
))}
</Flex>
<Flex flexDirection="column">
{theme === "light" ? (
<NavigationItem
title="Dark mode"
icon={DarkMode}
onClick={toggleNightMode}
/>
) : (
<NavigationItem
title="Light mode"
icon={LightMode}
onClick={toggleNightMode}
/>
)}
{isLoggedIn ? (
<>
<NavigationItem title="Sync" icon={Sync} />
</>
) : (
<NavigationItem title="Login" icon={Login} />
)}
{bottomRoutes.map((item) => (
<NavigationItem
key={item.path}
title={item.title}
icon={item.icon}
selected={location.startsWith(item.path)}
/>
))}
</Flex>
</Flex>
);
}
export default NavigationMenu;
function NavigationItem(props) {
const { icon: Icon, color, title, isLoading } = props;
const isTablet = useTablet();
return (
<Button
data-test-id={`navitem-${title.toLowerCase()}`}
variant="icon"
bg={props.selected ? "border" : "transparent"}
p={2}
mx={2}
mt={[1, 2, 1]}
sx={{
borderRadius: "default",
position: "relative",
":first-of-type": { mt: 2 },
":last-of-type": { mb: 2 },
}}
label={title}
title={title}
onClick={() => {
props.onClick();
}}
display="flex"
justifyContent={["flex-start", "center", "flex-start"]}
alignItems="center"
>
<Icon
size={isTablet ? 18 : 15}
color={color || (props.selected ? "primary" : "icon")}
rotate={isLoading}
/>
<Text
display={["block", "none", "block"]}
variant="body"
fontSize="subtitle"
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontWeight: props.selected ? "bold" : "normal",
}}
ml={1}
>
{title}
</Text>
</Button>
);
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { ReactComponent as Fav } from "../../assets/fav.svg";
import Fav from "../../assets/fav.svg";
import Placeholder from "./index";
function FavoritesPlaceholder() {

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from "react";
import { Button, Flex, Text } from "rebass";
import { Button, Flex, Image, Text } from "rebass";
import { useStore } from "../../stores/theme-store";
import { changeSvgTheme } from "../../utils/css";
@@ -8,7 +8,7 @@ function Placeholder(props) {
useEffect(() => {
changeSvgTheme(accent);
}, [accent]);
const { image: Image, text, callToAction } = props;
const { image: PlaceholderImage, text, callToAction } = props;
return (
<>
@@ -17,7 +17,7 @@ function Placeholder(props) {
alignSelf="stretch"
sx={{ position: "relative" }}
>
<Image width={"100%"} height={"100px"} />
<Image src={PlaceholderImage} width={"100%"} height={"100px"} />
<Text
variant="body"
mt={2}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { ReactComponent as Monographs } from "../../assets/monographs.svg";
import Monographs from "../../assets/monographs.svg";
import Placeholder from "./index";
function MonographsPlaceholder() {

View File

@@ -1,5 +1,5 @@
import React from "react";
import { ReactComponent as Notebook } from "../../assets/notebook.svg";
import Notebook from "../../assets/notebook.svg";
import Placeholder from "./index";
import * as Icon from "../icons";
import { hashNavigate } from "../../navigation";

View File

@@ -1,5 +1,5 @@
import React from "react";
import { ReactComponent as Note } from "../../assets/note.svg";
import Note from "../../assets/note.svg";
import Placeholder from "./index";
import * as Icon from "../icons";
import { hashNavigate } from "../../navigation";

View File

@@ -1,5 +1,5 @@
import React from "react";
import { ReactComponent as Search } from "../../assets/search.svg";
import Search from "../../assets/search.svg";
import Placeholder from "./index";
function SearchPlaceholder({ text }) {

View File

@@ -1,5 +1,5 @@
import React from "react";
import { ReactComponent as Tag } from "../../assets/tag.svg";
import Tag from "../../assets/tag.svg";
import Placeholder from "./index";
function TagsPlaceholder() {

View File

@@ -1,5 +1,5 @@
import React from "react";
import { ReactComponent as Notebook } from "../../assets/notebook.svg";
import Notebook from "../../assets/notebook.svg";
import Placeholder from "./index";
import * as Icon from "../icons";
import { hashNavigate } from "../../navigation";

View File

@@ -1,5 +1,5 @@
import React from "react";
import { ReactComponent as Trash } from "../../assets/trash.svg";
import Trash from "../../assets/trash.svg";
import Placeholder from "./index";
function TrashPlaceholder() {

View File

@@ -1,7 +1,7 @@
import { useAnimation } from "framer-motion";
import React, { useEffect } from "react";
import { Flex } from "rebass";
import Animated from "../animated";
import { AnimatedBox } from "../animated";
function ProgressBar(props) {
const { width, progress, duration = 1, onLoadingEnd, sx } = props;
@@ -12,7 +12,7 @@ function ProgressBar(props) {
return (
<Flex overflow="hidden" width={width} sx={sx}>
<Animated.Box
<AnimatedBox
height={5}
initial={{ width: "0%" }}
animate={animation}

View File

@@ -5,7 +5,7 @@ import { useStore } from "../../stores/editor-store";
import { AppEventManager, AppEvents, COLORS } from "../../common";
import { db } from "../../common/db";
import { useStore as useAppStore } from "../../stores/app-store";
import Animated from "../animated";
import { AnimatedFlex } from "../animated";
import Toggle from "./toggle";
import { navigate } from "../../navigation";
import IconTag from "../icon-tag";
@@ -77,7 +77,7 @@ function Properties({ noteId }) {
if (isFocusMode || !sessionId) return null;
return (
<>
<Animated.Flex
<AnimatedFlex
animate={{
x: arePropertiesVisible ? 0 : 800,
}}
@@ -325,7 +325,7 @@ function Properties({ noteId }) {
})}
</Card>
)}
</Animated.Flex>
</AnimatedFlex>
</>
);
}

View File

@@ -1,7 +1,7 @@
import React from "react";
import { Box, Button, Flex, Text } from "rebass";
import EditorFooter from "../editor/footer";
import * as Icon from "../icons";
import { Circle, Sync, Loading, Update } from "../icons";
import { useStore as useUserStore } from "../../stores/user-store";
import { useStore as useAppStore } from "../../stores/app-store";
import TimeAgo from "timeago-react";
@@ -43,7 +43,7 @@ function StatusBar() {
display="flex"
sx={{ alignItems: "center", justifyContent: "center" }}
>
<Icon.Circle
<Circle
size={7}
color={user.isEmailConfirmed ? "success" : "warn"}
/>
@@ -58,7 +58,7 @@ function StatusBar() {
onClick={sync}
sx={{ alignItems: "center", justifyContent: "center" }}
>
<Icon.Sync size={10} rotate={isSyncing} />
<Sync size={10} rotate={isSyncing} />
<Text variant="subBody" ml={1}>
{"Synced "}
{lastSynced ? (
@@ -76,7 +76,7 @@ function StatusBar() {
onClick={() => navigate("/login")}
sx={{ alignItems: "center", justifyContent: "center" }}
>
<Icon.Circle size={7} color="error" />
<Circle size={7} color="error" />
<Text variant="subBody" color="bgSecondaryText" ml={1}>
Not logged in
</Text>
@@ -84,7 +84,7 @@ function StatusBar() {
)}
{processingStatuses?.map(({ key, status, progress }) => (
<Flex key={key} ml={1} alignItems="center" justifyContent="center">
<Icon.Loading size={12} />
<Loading size={12} />
<Text variant="subBody" color="bgSecondaryText" ml={1}>
{progress ? `${progress}% ${status}` : status}
</Text>
@@ -106,7 +106,7 @@ function StatusBar() {
}}
sx={{ alignItems: "center", justifyContent: "center" }}
>
<Icon.Update
<Update
rotate={
updateStatus.type !== "updated" &&
updateStatus.type !== "completed" &&

View File

@@ -0,0 +1,27 @@
import { Box, Button, Text } from "rebass";
import { Circle } from "../icons";
function StatusBar() {
return (
<Box
bg="bgSecondary"
display={["none", "flex"]}
sx={{ borderTop: "1px solid", borderTopColor: "border" }}
justifyContent="space-between"
px={2}
>
<Button
variant="statusitem"
display="flex"
sx={{ alignItems: "center", justifyContent: "center" }}
>
<Circle size={7} color="error" />
<Text variant="subBody" color="bgSecondaryText" ml={1}>
Not logged in
</Text>
</Button>
</Box>
);
}
export default StatusBar;

View File

@@ -0,0 +1,16 @@
import { useEffect, useState } from "react";
import { initializeDatabase } from "../common/db";
export default function useDatabase() {
const [isAppLoaded, setIsAppLoaded] = useState(false);
useEffect(() => {
(async () => {
await import("../app.css");
await initializeDatabase();
setIsAppLoaded(true);
})();
}, []);
return [isAppLoaded];
}

View File

@@ -0,0 +1,18 @@
const { useState, useEffect } = require("react");
const { getCurrentPath, NavigationEvents } = require("../navigation");
export default function useLocation() {
const [location, setLocation] = useState(getCurrentPath());
useEffect(() => {
const navigateEvent = NavigationEvents.subscribe(
"onNavigate",
(_, location) => {
setLocation(location);
}
);
return () => {
navigateEvent.unsubscribe();
};
}, []);
return [location];
}

View File

@@ -1,7 +1,10 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
export default function useSlider({ initialIndex, onSliding, onChange }) {
const ref = useRef();
export default function useSlider(
sliderId,
{ initialIndex, onSliding, onChange }
) {
const ref = useRef(document.getElementById(sliderId));
const slides = useMemo(() => [], []);
useEffect(() => {
@@ -49,5 +52,5 @@ export default function useSlider({ initialIndex, onSliding, onChange }) {
[ref, slides]
);
return [ref, slideToIndex];
return [slideToIndex];
}

View File

@@ -1,65 +0,0 @@
html,
body {
font-size: 16px;
}
@media only screen and (max-width: 480px) {
html,
body {
font-size: 18px;
background-color: var(--background);
}
}
:root,
body,
#root {
margin: 0 !important;
height: 100%;
overflow: hidden;
}
* {
font-family: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
@keyframes fadeUp {
0% {
transform: translateY(500px);
opacity: 0;
}
80% {
transform: translateY(0px);
opacity: 0.7;
}
100% {
opacity: 1;
}
}
/* svg {
width: 1.5rem;
} */
.ReactModal__Overlay {
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.ReactModal__Overlay--after-open {
opacity: 1;
}
.ReactModal__Overlay--before-close {
opacity: 0;
}
.slide {
background-color: transparent !important;
}
.carousel.carousel-slider {
display: flex;
flex-direction: row;
}

View File

@@ -1,46 +1,46 @@
import "./commands";
import React from "react";
import { initializeDatabase } from "./common/db";
import "./index.css";
import { render } from "react-dom";
import { getCurrentPath } from "./navigation";
import * as serviceWorker from "./serviceWorkerRegistration";
import { loadTrackerScript } from "./utils/analytics";
import Config from "./utils/config";
import { isTesting } from "./utils/platform";
if (process.env.REACT_APP_PLATFORM === "desktop") require("./commands");
if (process.env.NODE_ENV === "production") {
loadTrackerScript();
console.log = () => {};
const ROUTES = {
"/account/recovery": {
component: () => import("./views/recovery"),
props: {},
},
"/account/verified": {
component: () => import("./views/email-confirmed"),
props: {},
},
"/signup": {
component: () => import("./views/auth"),
props: { type: "signup" },
},
"/login": {
component: () => import("./views/auth"),
props: { type: "login" },
},
"/recover": {
component: () => import("./views/auth"),
props: { type: "recover" },
},
default: { component: () => import("./app"), props: {} },
};
function getRoute() {
const path = getCurrentPath();
return ROUTES[path] || ROUTES.default;
}
const HOMEPAGE_ROUTE = { 1: "/notebooks", 2: "/favorites", 3: "/tags" };
async function checkRedirects(db) {
const isLoggedIn = !!(await db.user.getUser());
if (window.location.pathname === "/") {
const skipInitiation = Config.get("skipInitiation", false);
const homepage = Config.get("homepage", 0);
if (!isTesting() && !isLoggedIn && !skipInitiation)
window.location.replace("/signup");
else if (homepage) {
const route = HOMEPAGE_ROUTE[homepage];
window.location.replace(route);
const route = getRoute();
route.component().then(({ default: Component }) => {
render(
<Component {...route.props} />,
document.getElementById("root"),
() => {
document.getElementById("splash").remove();
}
}
}
initializeDatabase().then(async (db) => {
await checkRedirects(db);
import("react-dom").then(({ render }) => {
import("./App").then(({ default: App }) => {
render(<App />, document.getElementById("root"), async () => {
document.getElementById("splash").remove();
import("react-modal").then(({ default: Modal }) => {
Modal.setAppElement("#root");
});
});
});
});
);
});
// If you want your app to work offline and load faster, you can change

View File

@@ -122,7 +122,6 @@ async function hashStream(reader) {
}
async function readEncrypted(filename, key, cipherData) {
console.log("Reading encrypted file", filename);
const fileHandle = await streamablefs.readFile(filename);
if (!fileHandle) {
console.error(`File not found. Filename: ${filename}`);
@@ -159,8 +158,6 @@ async function readEncrypted(filename, key, cipherData) {
}
async function uploadFile(filename, requestOptions) {
console.log("Request to upload file", filename, requestOptions);
const fileHandle = await streamablefs.readFile(filename);
if (!fileHandle)
throw new Error(`File stream not found. Filename: ${filename}`);
@@ -261,7 +258,6 @@ function reportProgress(ev, { type, hash }) {
async function downloadFile(filename, requestOptions) {
const { url, headers, cancellationToken } = requestOptions;
console.log("Request to download file", filename, url, headers);
if (await streamablefs.exists(filename)) return true;
try {
@@ -273,7 +269,6 @@ async function downloadFile(filename, requestOptions) {
reportProgress(ev, { type: "download", hash: filename }),
});
console.log("File downloaded", filename, url, response);
if (!isSuccessStatusCode(response.status)) return false;
const distributor = new ChunkDistributor(ENCRYPTED_CHUNK_SIZE);
distributor.fill(new Uint8Array(response.data));
@@ -352,7 +347,6 @@ function cancellable(operation) {
return {
execute: () => operation(filename, requestOptions),
cancel: (message) => {
console.log("Canceled", message);
source.cancel(message);
},
};

View File

@@ -1,6 +1,6 @@
import { useState } from "react";
import EventManager from "notes-core/utils/eventmanager";
// import { useCallback } from "react";
import Config from "../utils/config";
export function navigate(url, replaceOrQuery, replace) {
if (typeof url !== "string") {
@@ -81,3 +81,13 @@ export function getCurrentHash() {
}
export const NavigationEvents = new EventManager();
const HOMEPAGE_ROUTE = {
0: "/notes",
1: "/notebooks",
2: "/favorites",
3: "/tags",
};
export function getHomeRoute() {
return HOMEPAGE_ROUTE[Config.get("homepage", 0)];
}

View File

@@ -1,14 +0,0 @@
import React from "react";
import AccountRecovery from "../views/recovery";
import EmailConfirmed from "../views/emailconfirmed";
import Auth from "../views/auth";
const rootroutes = {
"/account/recovery": () => ({ component: <AccountRecovery /> }),
"/account/verified": () => ({ component: <EmailConfirmed /> }),
"/signup": () => ({ component: <Auth type="signup" /> }),
"/login": () => ({ component: <Auth type="login" /> }),
"/recover": () => ({ component: <Auth type="recover" /> }),
};
export default rootroutes;

View File

@@ -15,7 +15,7 @@ import Monographs from "../views/monographs";
import { showToast } from "../utils/toast";
const routes = {
"/": () => ({
"/notes": () => ({
key: "home",
type: "notes",
title: "Notes",

View File

@@ -67,7 +67,6 @@ class UserStore extends BaseStore {
});
EV.subscribe(EVENTS.databaseSyncRequested, async () => {
console.log("Sync requested.");
await appStore.sync(false);
});

View File

@@ -1,27 +1,27 @@
import { useLocation } from "wouter";
import makeMatcher from "wouter/matcher";
import { navigate } from "../navigation";
import { store as selectionStore } from "../stores/selection-store";
import { navigate, getHomeRoute } from "../navigation";
export default function useRoutes(routes, options) {
const [location] = useLocation();
const matcher = makeMatcher();
// TODO move this to an extension function
if (selectionStore.get().isSelectionMode)
selectionStore.toggleSelectionMode(false);
if (location === "/") navigate(getHomeRoute());
options?.hooks?.beforeNavigate(location);
for (var key in routes) {
const [match, params] = matcher(key, location);
if (match) {
const result = routes[key](params);
if (!result) break;
return result;
return [result, location];
}
}
if (!options) return;
if (!options) return [];
const { fallbackRoute } = options;
if (fallbackRoute) {
navigate(fallbackRoute);
}
return [];
}

View File

@@ -1,12 +1,14 @@
import { useEffect, useState } from "react";
import { Box, Button, Flex, Text } from "rebass";
import ThemeProvider from "../components/theme-provider";
import * as Icon from "../components/icons";
import { CheckCircle, Loading, ArrowRight, Error } from "../components/icons";
import Field from "../components/field";
import { getQueryParams, navigate, useQueryParams } 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";
const features = [
{
@@ -90,7 +92,6 @@ const authTypes = {
</>
),
onSubmit: async (form, onError) => {
console.log(form);
if (form.password !== form.confirmPassword) {
onError("Passwords do not match.");
return;
@@ -162,6 +163,7 @@ function Auth(props) {
const [error, setError] = useState();
const [success, setSuccess] = useState();
const [featureIndex, setFeatureIndex] = useState(0);
const [isAppLoaded] = useDatabase();
const data = authTypes[type];
const feature = features[featureIndex];
@@ -188,7 +190,7 @@ function Auth(props) {
setSuccess();
if (authTypes[type].resetOnNavigate) {
const form = document.getElementById("authForm");
form.reset();
form?.reset();
}
}, [type]);
@@ -211,7 +213,7 @@ function Auth(props) {
navigate("/");
}}
>
<Icon.ArrowRight size={16} />
<ArrowRight size={16} />
<Text variant="body" ml={1}>
Go to app
</Text>
@@ -262,7 +264,7 @@ function Auth(props) {
mt={4}
alignItems="center"
>
<Icon.ArrowRight size={18} color="static" />
<ArrowRight size={18} color="static" />
<Text variant="title" ml={1} color="static">
{feature.linkText}
</Text>
@@ -285,151 +287,171 @@ function Auth(props) {
</Flex>
</Box>
<Flex justifyContent="center" flex={1} flexShrink={0}>
<Flex
as="form"
id="authForm"
flexDirection="column"
justifyContent="center"
alignItems="stretch"
width={["95%", "55%", "35%"]}
onSubmit={async (e) => {
e.preventDefault();
setIsSubmitting(true);
const form = new FormData(e.target);
const obj = Object.fromEntries(form.entries());
obj.redirect = redirect;
await data.onSubmit(
obj,
(error) => {
setIsSubmitting(false);
setError(error);
},
setSuccess
);
setIsSubmitting(false);
}}
>
<Text variant="heading" textAlign="center" fontWeight="heading">
{data.title}
</Text>
<Text variant="body" mt={1} textAlign="center" color="fontTertiary">
{data.subtitle.text}{" "}
{data.subtitle.action && (
<Text
sx={{ ":hover": { color: "dimPrimary" }, cursor: "pointer" }}
as="b"
color="primary"
onClick={data.subtitle.action.onClick}
>
{data.subtitle.action.text}
</Text>
)}
</Text>
<Field
styles={{
container: { mt: 50 },
}}
data-test-id="email"
id="email"
required
name="email"
autoComplete="email"
label={data.labels.email}
helpText={data.helpTexts?.email}
/>
{data.labels.password && (
<Field
styles={{
container: { mt: 2 },
}}
data-test-id="password"
id="password"
required
name="password"
autoComplete={data.autoComplete?.password}
label={data.labels.password}
type="password"
/>
)}
{data.confirmPassword && (
<Field
styles={{
container: { mt: 2 },
}}
data-test-id="confirm-password"
id="confirmPassword"
required
name="confirmPassword"
autoComplete="confirm-password"
label="Confirm your password"
type="password"
/>
)}
{data.supportsPasswordRecovery && (
<Button
type="button"
alignSelf="start"
mt={2}
variant="anchor"
onClick={() => navigate("/recover", getQueryParams())}
>
Forgot password?
</Button>
)}
<Button
data-test-id="submitButton"
display="flex"
type="submit"
mt={4}
variant="primary"
disabled={isSubmitting}
sx={{ borderRadius: "default" }}
{isAppLoaded ? (
<Flex
as="form"
id="authForm"
flexDirection="column"
justifyContent="center"
alignItems="center"
alignItems="stretch"
width={["95%", "55%", "35%"]}
onSubmit={async (e) => {
e.preventDefault();
setIsSubmitting(true);
const form = new FormData(e.target);
const obj = Object.fromEntries(form.entries());
obj.redirect = redirect;
await data.onSubmit(
obj,
(error) => {
setIsSubmitting(false);
setError(error);
},
setSuccess
);
setIsSubmitting(false);
}}
>
{isSubmitting ? (
<>
<Icon.Loading color="static" size={16} sx={{ mr: 1 }} />{" "}
{data.primaryAction.loadingText}
</>
) : (
data.primaryAction.text
)}
</Button>
{data.secondaryAction && (
<Button
type="button"
variant="secondary"
mt={1}
onClick={data.secondaryAction.onClick}
>
{data.secondaryAction.text}
</Button>
)}
{error && (
<Flex bg="errorBg" p={1} mt={2} sx={{ borderRadius: "default" }}>
<Icon.Error size={15} color="error" />
<Text variant="error" ml={1}>
{error}
</Text>
</Flex>
)}
{success && (
<Flex bg="shade" p={1} mt={2} sx={{ borderRadius: "default" }}>
<Icon.CheckCircle size={15} color="primary" />
<Text variant="error" color="primary" ml={1}>
{success}
</Text>
</Flex>
)}
{data.agreementText && (
<Text mt={2} variant="subBody">
{data.agreementText}
<Text variant="heading" textAlign="center" fontWeight="heading">
{data.title}
</Text>
)}
</Flex>
<Text
variant="body"
mt={1}
textAlign="center"
color="fontTertiary"
>
{data.subtitle.text}{" "}
{data.subtitle.action && (
<Text
sx={{
":hover": { color: "dimPrimary" },
cursor: "pointer",
}}
as="b"
color="primary"
onClick={data.subtitle.action.onClick}
>
{data.subtitle.action.text}
</Text>
)}
</Text>
<Field
styles={{
container: { mt: 50 },
}}
data-test-id="email"
id="email"
required
name="email"
autoComplete="email"
label={data.labels.email}
helpText={data.helpTexts?.email}
/>
{data.labels.password && (
<Field
styles={{
container: { mt: 2 },
}}
data-test-id="password"
id="password"
required
name="password"
autoComplete={data.autoComplete?.password}
label={data.labels.password}
type="password"
/>
)}
{data.confirmPassword && (
<Field
styles={{
container: { mt: 2 },
}}
data-test-id="confirm-password"
id="confirmPassword"
required
name="confirmPassword"
autoComplete="confirm-password"
label="Confirm your password"
type="password"
/>
)}
{data.supportsPasswordRecovery && (
<Button
type="button"
alignSelf="start"
mt={2}
variant="anchor"
onClick={() => navigate("/recover", getQueryParams())}
>
Forgot password?
</Button>
)}
<Button
data-test-id="submitButton"
display="flex"
type="submit"
mt={4}
variant="primary"
disabled={isSubmitting}
sx={{ borderRadius: "default" }}
justifyContent="center"
alignItems="center"
>
{isSubmitting ? (
<>
<Loading color="static" size={16} sx={{ mr: 1 }} />{" "}
{data.primaryAction.loadingText}
</>
) : (
data.primaryAction.text
)}
</Button>
{data.secondaryAction && (
<Button
type="button"
variant="secondary"
mt={1}
onClick={data.secondaryAction.onClick}
>
{data.secondaryAction.text}
</Button>
)}
{error && (
<Flex
bg="errorBg"
p={1}
mt={2}
sx={{ borderRadius: "default" }}
>
<Error size={15} color="error" />
<Text variant="error" ml={1}>
{error}
</Text>
</Flex>
)}
{success && (
<Flex bg="shade" p={1} mt={2} sx={{ borderRadius: "default" }}>
<CheckCircle size={15} color="primary" />
<Text variant="error" color="primary" ml={1}>
{success}
</Text>
</Flex>
)}
{data.agreementText && (
<Text mt={2} variant="subBody">
{data.agreementText}
</Text>
)}
</Flex>
) : (
<Loader
title="Did you know?"
text="Your password never leaves your device. Ever."
/>
)}
</Flex>
</Flex>
</ThemeProvider>

View File

@@ -1,3 +1,4 @@
import "../app.css";
import { useEffect } from "react";
import { Flex, Text } from "rebass";
import ThemeProvider from "../components/theme-provider";

View File

@@ -10,6 +10,7 @@ import { useCallback } from "react";
import { showRecoveryKeyDialog } from "../common/dialog-controller";
import { createBackup } from "../common";
import Logo from "../assets/logo.svg";
import useDatabase from "../hooks/use-database";
function navigate(path) {
window.location.href = path;
@@ -42,7 +43,9 @@ function useRecovery() {
}
function useAuthenticateUser({ code, userId, performAction }) {
const [isAppLoaded] = useDatabase();
useEffect(() => {
if (!isAppLoaded) return;
performAction({
message: "Authenticating. Please wait...",
error: "Failed to authenticate. Please try again.",
@@ -63,7 +66,7 @@ function useAuthenticateUser({ code, userId, performAction }) {
}
await db.user.fetchUser(true);
}
}, [code, userId, performAction]);
}, [code, userId, performAction, isAppLoaded]);
}
const steps = [RecoveryKeyStep, BackupDataStep, NewPasswordStep, FinalStep];