mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 11:47:54 +01:00
feat: add backup reminders
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
"@mdi/react": "^1.4.0",
|
||||
"@rebass/forms": "^4.0.6",
|
||||
"cogo-toast": "^4.2.3",
|
||||
"dayjs": "^1.9.1",
|
||||
"emotion-theming": "^10.0.19",
|
||||
"eventsource": "^1.0.7",
|
||||
"fast-sort": "^2.1.1",
|
||||
@@ -81,4 +82,4 @@
|
||||
"last 4 edge version"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,16 @@ import routes from "./navigation/routes";
|
||||
import Editor from "./components/editor";
|
||||
import useMobile from "./utils/use-mobile";
|
||||
import GlobalMenuWrapper from "./components/globalmenuwrapper";
|
||||
import {
|
||||
shouldAddBackupReminder,
|
||||
shouldAddSignupReminder,
|
||||
} from "./common/reminders";
|
||||
|
||||
function App() {
|
||||
const [show, setShow] = usePersistentState("isContainerVisible", true);
|
||||
const refreshColors = useStore((store) => store.refreshColors);
|
||||
const isFocusMode = useStore((store) => store.isFocusMode);
|
||||
const addReminder = useStore((store) => store.addReminder);
|
||||
const initUser = useUserStore((store) => store.init);
|
||||
const initNotes = useNotesStore((store) => store.init);
|
||||
const openLastSession = useEditorStore((store) => store.openLastSession);
|
||||
@@ -27,11 +32,22 @@ function App() {
|
||||
const isMobile = useMobile();
|
||||
const routeResult = useRoutes(routes);
|
||||
|
||||
useEffect(() => {
|
||||
refreshColors();
|
||||
initUser();
|
||||
initNotes();
|
||||
}, [refreshColors, initUser, initNotes]);
|
||||
useEffect(
|
||||
function initializeApp() {
|
||||
refreshColors();
|
||||
initUser();
|
||||
initNotes();
|
||||
(async function () {
|
||||
if (await shouldAddBackupReminder()) {
|
||||
addReminder("backup", "high");
|
||||
}
|
||||
if (await shouldAddSignupReminder()) {
|
||||
addReminder("signup", "low");
|
||||
}
|
||||
})();
|
||||
},
|
||||
[refreshColors, initUser, initNotes, addReminder]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocusMode) {
|
||||
|
||||
46
apps/web/src/common/reminders.js
Normal file
46
apps/web/src/common/reminders.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import Config from "../utils/config";
|
||||
import { db } from "./index";
|
||||
import * as Icon from "../components/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { showSignUpDialog } from "../components/dialogs/signupdialog";
|
||||
import download from "../utils/download";
|
||||
|
||||
export async function shouldAddBackupReminder() {
|
||||
const backupReminderOffset = Config.get("backupReminderOffset", 0);
|
||||
if (backupReminderOffset) return false;
|
||||
|
||||
const lastBackupTime = await db.backup.lastBackupTime();
|
||||
const offsetToDays =
|
||||
backupReminderOffset === 1 ? 1 : backupReminderOffset === 2 ? 7 : 30;
|
||||
return dayjs(lastBackupTime).add(offsetToDays, "d").isAfter(dayjs());
|
||||
}
|
||||
|
||||
export async function shouldAddSignupReminder() {
|
||||
const user = await db.user.get();
|
||||
if (!user) return true;
|
||||
}
|
||||
|
||||
export const Reminders = {
|
||||
backup: {
|
||||
title: "Back up your data now!",
|
||||
action: {
|
||||
text: "Click here to backup!",
|
||||
onClick: async () => {
|
||||
download(
|
||||
`notesnook-backup-${new Date().toLocaleString("en")}`,
|
||||
await db.backup.export(),
|
||||
"nnbackup"
|
||||
);
|
||||
},
|
||||
},
|
||||
icon: Icon.Backup,
|
||||
},
|
||||
signup: {
|
||||
title: "Sign up for cross-device syncing and so much more!",
|
||||
action: {
|
||||
text: "Click here to sign up!",
|
||||
onClick: () => showSignUpDialog(),
|
||||
},
|
||||
icon: Icon.User,
|
||||
},
|
||||
};
|
||||
@@ -100,3 +100,5 @@ export const Info = createIcon(Icons.mdiInformation);
|
||||
|
||||
export const ToggleUnchecked = createIcon(Icons.mdiToggleSwitchOff);
|
||||
export const ToggleChecked = createIcon(Icons.mdiToggleSwitch);
|
||||
|
||||
export const Backup = createIcon(Icons.mdiBackupRestore);
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as Icon from "../icons";
|
||||
import { VariableSizeList as List } from "react-window";
|
||||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
import { useStore as useSelectionStore } from "../../stores/selection-store";
|
||||
import LoginBar from "../loginbar";
|
||||
import ReminderBar from "../reminder-bar";
|
||||
import GroupHeader from "../group-header";
|
||||
import ListProfiles from "../../common/list-profiles";
|
||||
|
||||
@@ -39,7 +39,7 @@ function ListContainer(props) {
|
||||
) : (
|
||||
<>
|
||||
<Search type={props.type} query={props.query} context={context} />
|
||||
<LoginBar />
|
||||
<ReminderBar />
|
||||
<Flex variant="columnFill" mt={2} data-test-id="note-list">
|
||||
{props.children
|
||||
? props.children
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from "react";
|
||||
import { Flex, Text } from "rebass";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { getRandom } from "../../utils/random";
|
||||
import { showLogInDialog } from "../dialogs/logindialog";
|
||||
|
||||
const data = [
|
||||
"Login to start your 14-day free trial",
|
||||
"Login to sync your data",
|
||||
];
|
||||
|
||||
function LoginBar() {
|
||||
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
|
||||
|
||||
if (isLoggedIn) return null;
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
p={2}
|
||||
mt={2}
|
||||
width="100%"
|
||||
bg="shade"
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={showLogInDialog}
|
||||
>
|
||||
<Text variant="subBody">Click here to login</Text>
|
||||
<Text variant="body" color="primary">
|
||||
{data[getRandom(0, data.length - 1)]}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default LoginBar;
|
||||
38
apps/web/src/components/reminder-bar/index.js
Normal file
38
apps/web/src/components/reminder-bar/index.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Flex, Text, Box } from "rebass";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { Reminders } from "../../common/reminders";
|
||||
|
||||
function ReminderBar() {
|
||||
const reminders = useAppStore((store) => store.reminders);
|
||||
const reminder = useMemo(() => {
|
||||
const reminder = reminders.sort((a, b) => a.priority - b.priority)[0];
|
||||
if (!reminder) return;
|
||||
return Reminders[reminder.type];
|
||||
}, [reminders]);
|
||||
if (!reminder) return null;
|
||||
return (
|
||||
<Flex
|
||||
p={2}
|
||||
mt={2}
|
||||
bg={"shade"}
|
||||
alignItems="center"
|
||||
mx={2}
|
||||
sx={{ cursor: "pointer", borderRadius: "default" }}
|
||||
onClick={reminder?.action?.onClick}
|
||||
>
|
||||
<Box sx={{ bg: "primary", borderRadius: 80 }} width={40} p={2} mr={2}>
|
||||
<reminder.icon size={18} color="static" />
|
||||
</Box>
|
||||
<Flex flexDirection="column">
|
||||
<Text variant="subBody" fontSize={10}>
|
||||
{reminder.action.text}
|
||||
</Text>
|
||||
<Text variant="body" fontSize={12} color="primary">
|
||||
{reminder.title}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default ReminderBar;
|
||||
@@ -8,14 +8,14 @@ import App from "./App";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import Modal from "react-modal";
|
||||
import { db } from "./common";
|
||||
import { MotionConfig, AnimationFeature } from "framer-motion";
|
||||
import { MotionConfig, AnimationFeature, GesturesFeature } from "framer-motion";
|
||||
|
||||
db.init()
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
Modal.setAppElement("#root");
|
||||
ReactDOM.render(
|
||||
<MotionConfig features={[AnimationFeature]}>
|
||||
<MotionConfig features={[AnimationFeature, GesturesFeature]}>
|
||||
<App />
|
||||
</MotionConfig>,
|
||||
document.getElementById("root")
|
||||
|
||||
@@ -14,6 +14,7 @@ class AppStore extends BaseStore {
|
||||
isEditorOpen = false;
|
||||
colors = [];
|
||||
globalMenu = { items: [], data: {} };
|
||||
reminders = [];
|
||||
|
||||
refresh = async () => {
|
||||
noteStore.refresh();
|
||||
@@ -47,6 +48,22 @@ class AppStore extends BaseStore {
|
||||
setIsEditorOpen = (toggleState) => {
|
||||
this.set((state) => (state.isEditorOpen = toggleState));
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"backup"|"signup"} type
|
||||
* @param {string} title
|
||||
* @param {string} detail
|
||||
* @param {"high"|"medium"|"low"} priority
|
||||
*/
|
||||
addReminder = (type, priority) => {
|
||||
this.set((state) =>
|
||||
state.reminders.push({
|
||||
type,
|
||||
priority: priority === "high" ? 1 : priority === "medium" ? 2 : 1,
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Button, Flex, Text } from "rebass";
|
||||
import React, { useEffect } from "react";
|
||||
import { Button, Flex, Text } from "rebass";
|
||||
import * as Icon from "../components/icons";
|
||||
import { useStore as useUserStore } from "../stores/user-store";
|
||||
import { useStore as useThemeStore } from "../stores/theme-store";
|
||||
@@ -10,12 +10,43 @@ import { upgrade } from "../common/upgrade";
|
||||
import useSystemTheme from "../utils/use-system-theme";
|
||||
import download from "../utils/download";
|
||||
import { db } from "../common";
|
||||
import { usePersistentState } from "../utils/hooks";
|
||||
|
||||
function importBackup() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const importFileElem = document.getElementById("restore-backup");
|
||||
importFileElem.click();
|
||||
importFileElem.onchange = function () {
|
||||
const file = importFileElem.files[0];
|
||||
if (!file.name.endsWith(".nnbackup")) {
|
||||
alert(
|
||||
"Invalid backup file provided. Make sure it has an .nnbackup extension."
|
||||
);
|
||||
return reject(
|
||||
"The given file does not have .nnbackup extension. Only files with .nnbackup extension are supported."
|
||||
);
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", (event) => {
|
||||
const text = event.target.result;
|
||||
try {
|
||||
resolve(JSON.parse(text));
|
||||
} catch (e) {
|
||||
alert(
|
||||
"Error: Could not read the backup file provided. Either it's corrupted or invalid."
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
reader.readAsText(file);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function Settings(props) {
|
||||
const theme = useThemeStore((store) => store.theme);
|
||||
const toggleNightMode = useThemeStore((store) => store.toggleNightMode);
|
||||
const setTheme = useThemeStore((store) => store.setTheme);
|
||||
|
||||
const preferSystemTheme = useThemeStore((store) => store.preferSystemTheme);
|
||||
const togglePreferSystemTheme = useThemeStore(
|
||||
(store) => store.togglePreferSystemTheme
|
||||
@@ -26,6 +57,10 @@ function Settings(props) {
|
||||
(store) => store?.user?.notesnook?.subscription?.isTrial
|
||||
);
|
||||
const logout = useUserStore((store) => store.logout);
|
||||
const [backupReminderOffset, setBackupReminderOffset] = usePersistentState(
|
||||
"backupReminderOffset",
|
||||
0
|
||||
);
|
||||
|
||||
const isSystemThemeDark = useSystemTheme();
|
||||
useEffect(() => {
|
||||
@@ -174,7 +209,18 @@ function Settings(props) {
|
||||
tip="Backup and download all your data"
|
||||
/>
|
||||
</Button>
|
||||
<Button variant="list">
|
||||
<input
|
||||
type="file"
|
||||
id="restore-backup"
|
||||
hidden
|
||||
accept=".nnbackup,text/plain,application/json"
|
||||
/>
|
||||
<Button
|
||||
variant="list"
|
||||
onClick={async () => {
|
||||
await db.backup.import(JSON.stringify(await importBackup()));
|
||||
}}
|
||||
>
|
||||
<TextWithTip
|
||||
text="Restore backup"
|
||||
tip="Restore data from a backup file"
|
||||
@@ -187,6 +233,14 @@ function Settings(props) {
|
||||
offTip="Backups will not be encrypted"
|
||||
/>
|
||||
|
||||
<OptionsItem
|
||||
title="Backup reminders"
|
||||
tip="Remind me to backup my data"
|
||||
options={["Never", "Daily", "Weekly", "Monthly"]}
|
||||
selectedOption={backupReminderOffset}
|
||||
onSelectionChanged={(_option, index) => setBackupReminderOffset(index)}
|
||||
/>
|
||||
|
||||
<Text
|
||||
variant="subtitle"
|
||||
color="primary"
|
||||
@@ -250,3 +304,56 @@ function ToggleItem(props) {
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function OptionsItem(props) {
|
||||
const {
|
||||
title,
|
||||
tip,
|
||||
options,
|
||||
selectedOption,
|
||||
onSelectionChanged,
|
||||
onlyIf,
|
||||
} = props;
|
||||
|
||||
if (onlyIf === false) return null;
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
py={2}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
borderBottom: "1px solid",
|
||||
borderBottomColor: "border",
|
||||
":hover": { borderBottomColor: "primary" },
|
||||
}}
|
||||
>
|
||||
<TextWithTip text={title} tip={tip} />
|
||||
<Flex
|
||||
justifyContent="space-evenly"
|
||||
mt={2}
|
||||
bg="border"
|
||||
sx={{ borderRadius: "default", overflow: "hidden" }}
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<Text
|
||||
key={option}
|
||||
flex={1}
|
||||
bg={selectedOption === index ? "primary" : "transparent"}
|
||||
color={selectedOption === index ? "static" : "gray"}
|
||||
textAlign="center"
|
||||
variant="subBody"
|
||||
p={2}
|
||||
py={1}
|
||||
onClick={() => onSelectionChanged(option, index)}
|
||||
sx={{
|
||||
":hover": { color: selectedOption === index ? "static" : "text" },
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
</Text>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4268,6 +4268,11 @@ data-urls@^2.0.0:
|
||||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
dayjs@^1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.9.1.tgz#201a755f7db5103ed6de63ba93a984141c754541"
|
||||
integrity sha512-01NCTBg8cuMJG1OQc6PR7T66+AFYiPwgDvdJmvJBn29NGzIG+DIFxPLNjHzwz3cpFIvG+NcwIjP9hSaPVoOaDg==
|
||||
|
||||
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
@@ -7962,6 +7967,11 @@ jsesc@~0.5.0:
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
||||
integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
|
||||
|
||||
jshashes@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/jshashes/-/jshashes-1.0.8.tgz#f60d837428383abf73ab022e1542e6614bd75514"
|
||||
integrity sha512-btmQZ/w1rj8Lb6nEwvhjM7nBYoj54yaEFo2PWh3RkxZ8qNwuvOxvQYN/JxVuwoMmdIluL+XwYVJ+pEEZoSYybQ==
|
||||
|
||||
json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
||||
@@ -8851,10 +8861,11 @@ normalize-url@^3.0.0, normalize-url@^3.0.1:
|
||||
|
||||
"notes-core@git+ssh://git@github.com:streetwriters/notesnook-core.git":
|
||||
version "1.5.0"
|
||||
resolved "git+ssh://git@github.com:streetwriters/notesnook-core.git#c2e98551b4a45734b62878af3b9c8cea373456b3"
|
||||
resolved "git+ssh://git@github.com:streetwriters/notesnook-core.git#22639feabcf9ee9c6f8aa8d4edd9fc1f5e13fa73"
|
||||
dependencies:
|
||||
fast-sort "^2.0.1"
|
||||
fuzzysearch "^1.0.3"
|
||||
jshashes "^1.0.8"
|
||||
no-internet "^1.5.2"
|
||||
qclone "^1.0.4"
|
||||
quill-delta-to-html "^0.12.0"
|
||||
|
||||
Reference in New Issue
Block a user