feat: add backup reminders

This commit is contained in:
thecodrr
2020-10-03 14:52:22 +05:00
parent 484438e7d4
commit 3eb5f258d7
11 changed files with 253 additions and 48 deletions

View File

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

View File

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

View 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,
},
};

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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