web: add support for setting user profile

This commit is contained in:
Abdullah Atta
2024-02-28 23:21:52 +05:00
parent e58fc38a6d
commit 54ba654b97
9 changed files with 442 additions and 128 deletions

View File

@@ -67,6 +67,7 @@
"platform": "^1.3.6",
"qclone": "^1.2.0",
"react": "18.2.0",
"react-avatar-editor": "^13.0.2",
"react-complex-tree": "^2.2.4",
"react-day-picker": "^8.9.1",
"react-dom": "18.2.0",
@@ -97,6 +98,7 @@
"@types/node-fetch": "^2.5.10",
"@types/platform": "^1.3.4",
"@types/react": "^18.2.39",
"@types/react-avatar-editor": "^13.0.2",
"@types/react-dom": "^18.2.17",
"@types/react-modal": "3.16.3",
"@types/tinycolor2": "^1.4.3",
@@ -34707,7 +34709,6 @@
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
@@ -34845,7 +34846,6 @@
},
"node_modules/@babel/compat-data": {
"version": "7.23.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -34853,7 +34853,6 @@
},
"node_modules/@babel/core": {
"version": "7.23.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
@@ -34882,7 +34881,6 @@
},
"node_modules/@babel/generator": {
"version": "7.23.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.23.3",
@@ -34918,7 +34916,6 @@
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.22.15",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.9",
@@ -34986,7 +34983,6 @@
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.22.20",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -34994,7 +34990,6 @@
},
"node_modules/@babel/helper-function-name": {
"version": "7.23.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.22.15",
@@ -35006,7 +35001,6 @@
},
"node_modules/@babel/helper-hoist-variables": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.22.5"
@@ -35027,10 +35021,11 @@
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.22.15",
"license": "MIT",
"version": "7.24.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
"integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
"dependencies": {
"@babel/types": "^7.22.15"
"@babel/types": "^7.24.0"
},
"engines": {
"node": ">=6.9.0"
@@ -35038,7 +35033,6 @@
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.23.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.20",
@@ -35066,9 +35060,9 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz",
"integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==",
"engines": {
"node": ">=6.9.0"
}
@@ -35107,7 +35101,6 @@
},
"node_modules/@babel/helper-simple-access": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.22.5"
@@ -35129,7 +35122,6 @@
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.22.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.22.5"
@@ -35139,8 +35131,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.22.5",
"license": "MIT",
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
"integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
"engines": {
"node": ">=6.9.0"
}
@@ -35154,7 +35147,6 @@
},
"node_modules/@babel/helper-validator-option": {
"version": "7.22.15",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -35175,7 +35167,6 @@
},
"node_modules/@babel/helpers": {
"version": "7.23.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.22.15",
@@ -35257,7 +35248,6 @@
},
"node_modules/@babel/parser": {
"version": "7.23.3",
"dev": true,
"license": "MIT",
"bin": {
"parser": "bin/babel-parser.js"
@@ -36133,6 +36123,63 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-runtime": {
"version": "7.24.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.3.tgz",
"integrity": "sha512-J0BuRPNlNqlMTRJ72eVptpt9VcInbxO6iP3jaxr+1NPhC0UkKL+6oeX6VXMEYdADnuqmMmsBspt4d5w8Y/TCbQ==",
"dependencies": {
"@babel/helper-module-imports": "^7.24.3",
"@babel/helper-plugin-utils": "^7.24.0",
"babel-plugin-polyfill-corejs2": "^0.4.10",
"babel-plugin-polyfill-corejs3": "^0.10.1",
"babel-plugin-polyfill-regenerator": "^0.6.1",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-runtime/node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz",
"integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==",
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
"@babel/helper-plugin-utils": "^7.22.5",
"debug": "^4.1.1",
"lodash.debounce": "^4.0.8",
"resolve": "^1.14.2"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs3": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz",
"integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.1",
"core-js-compat": "^3.36.1"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-regenerator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz",
"integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.1"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/@babel/plugin-transform-shorthand-properties": {
"version": "7.23.3",
"dev": true,
@@ -36386,7 +36433,6 @@
},
"node_modules/@babel/template": {
"version": "7.22.15",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.22.13",
@@ -36399,7 +36445,6 @@
},
"node_modules/@babel/traverse": {
"version": "7.23.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.22.13",
@@ -36418,10 +36463,11 @@
}
},
"node_modules/@babel/types": {
"version": "7.23.3",
"license": "MIT",
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
"integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
@@ -36632,7 +36678,6 @@
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
@@ -36645,7 +36690,6 @@
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -36653,7 +36697,6 @@
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -36670,12 +36713,10 @@
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.20",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -38049,6 +38090,15 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-avatar-editor": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/@types/react-avatar-editor/-/react-avatar-editor-13.0.2.tgz",
"integrity": "sha512-vnGU4sx5TDB9JMCuw+kB3+jfDNMhVlpBbgMRZG9NzVnaBb5xUjVV4VmHdD2O8gj/pb5GN+QXKk7jan09aMjG2A==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-dom": {
"version": "18.2.19",
"dev": true,
@@ -38658,18 +38708,33 @@
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.6",
"dev": true,
"license": "MIT",
"version": "0.4.10",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz",
"integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==",
"dependencies": {
"@babel/compat-data": "^7.22.6",
"@babel/helper-define-polyfill-provider": "^0.4.3",
"@babel/helper-define-polyfill-provider": "^0.6.1",
"semver": "^6.3.1"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/babel-plugin-polyfill-corejs2/node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz",
"integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==",
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
"@babel/helper-plugin-utils": "^7.22.5",
"debug": "^4.1.1",
"lodash.debounce": "^4.0.8",
"resolve": "^1.14.2"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/babel-plugin-polyfill-corejs3": {
"version": "0.8.6",
"dev": true,
@@ -38808,8 +38873,9 @@
}
},
"node_modules/browserslist": {
"version": "4.22.1",
"dev": true,
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"funding": [
{
"type": "opencollective",
@@ -38824,11 +38890,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001541",
"electron-to-chromium": "^1.4.535",
"node-releases": "^2.0.13",
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
"bin": {
@@ -38954,8 +39019,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001562",
"dev": true,
"version": "1.0.30001599",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
"integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
"funding": [
{
"type": "opencollective",
@@ -38969,8 +39035,7 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
]
},
"node_modules/canvas": {
"version": "2.11.2",
@@ -39184,15 +39249,14 @@
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/core-js-compat": {
"version": "3.33.2",
"dev": true,
"license": "MIT",
"version": "3.36.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz",
"integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==",
"dependencies": {
"browserslist": "^4.22.1"
"browserslist": "^4.23.0"
},
"funding": {
"type": "opencollective",
@@ -39572,9 +39636,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.585",
"dev": true,
"license": "ISC"
"version": "1.4.713",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.713.tgz",
"integrity": "sha512-vDarADhwntXiULEdmWd77g2dV6FrNGa8ecAC29MZ4TwPut2fvosD0/5sJd1qWNNe8HcJFAC+F5Lf9jW1NPtWmw=="
},
"node_modules/electron-trpc": {
"version": "0.5.2",
@@ -39955,7 +40019,6 @@
},
"node_modules/escalade": {
"version": "3.1.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -40400,7 +40463,6 @@
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -40513,7 +40575,6 @@
},
"node_modules/globals": {
"version": "11.12.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -41317,7 +41378,6 @@
},
"node_modules/jsesc": {
"version": "2.5.2",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -41353,7 +41413,6 @@
},
"node_modules/json5": {
"version": "2.2.3",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@@ -41680,7 +41739,6 @@
},
"node_modules/lru-cache": {
"version": "5.1.1",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
@@ -42771,9 +42829,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"dev": true,
"license": "MIT"
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
},
"node_modules/nopt": {
"version": "5.0.0",
@@ -43029,7 +43087,6 @@
},
"node_modules/picocolors": {
"version": "1.0.0",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -43331,6 +43388,20 @@
"node": ">=0.10.0"
}
},
"node_modules/react-avatar-editor": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/react-avatar-editor/-/react-avatar-editor-13.0.2.tgz",
"integrity": "sha512-a4ajbi7lwDh98kgEtSEeKMu0vs0CHTczkq4Xcxr1EiwMFH1GlgHCEtwGU8q/H5W8SeLnH4KPK8LUjEEaZXklxQ==",
"dependencies": {
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/runtime": "^7.12.5",
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-complex-tree": {
"version": "2.3.4",
"license": "MIT",
@@ -44916,7 +44987,6 @@
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -45902,7 +45972,6 @@
},
"node_modules/yallist": {
"version": "3.1.1",
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {

View File

@@ -65,6 +65,7 @@
"platform": "^1.3.6",
"qclone": "^1.2.0",
"react": "18.2.0",
"react-avatar-editor": "^13.0.2",
"react-complex-tree": "^2.2.4",
"react-day-picker": "^8.9.1",
"react-dom": "18.2.0",
@@ -95,6 +96,7 @@
"@types/node-fetch": "^2.5.10",
"@types/platform": "^1.3.4",
"@types/react": "^18.2.39",
"@types/react-avatar-editor": "^13.0.2",
"@types/react-dom": "^18.2.17",
"@types/react-modal": "3.16.3",
"@types/tinycolor2": "^1.4.3",

View File

@@ -33,8 +33,13 @@ import { ConfirmDialogProps } from "../dialogs/confirm";
import { getFormattedDate } from "@notesnook/common";
import { downloadUpdate } from "../utils/updater";
import { ThemeMetadata } from "@notesnook/themes-server";
import { Color, Reminder, Tag } from "@notesnook/core";
import { AuthenticatorType } from "@notesnook/core/dist/api/user-manager";
import {
Color,
Profile,
Reminder,
Tag,
AuthenticatorType
} from "@notesnook/core";
import { createRoot } from "react-dom/client";
import { PasswordDialogProps } from "../dialogs/password-dialog";
@@ -476,6 +481,12 @@ export function showAttachmentsDialog() {
));
}
export function showEditProfileDialog(profile?: Profile) {
return showDialog("EditProfileDialog", (Dialog, perform) => (
<Dialog onClose={(res: boolean) => perform(res)} profile={profile} />
));
}
export function showSettings() {
return showDialog("SettingsDialog", (Dialog, perform) => (
<Dialog onClose={(res: boolean) => perform(res)} />

View File

@@ -35,7 +35,8 @@ import {
Login,
Circle,
Icon,
Reminders
Reminders,
User
} from "../icons";
import { AnimatedFlex } from "../animated";
import NavigationItem, { SortableNavigationItem } from "./navigation-item";
@@ -134,6 +135,7 @@ function NavigationMenu(props: NavigationMenuProps) {
const shortcuts = useAppStore((store) => store.shortcuts);
const refreshNavItems = useAppStore((store) => store.refreshNavItems);
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
const profile = useUserStore((store) => store.profile);
const isMobile = useMobile();
const theme = useThemeStore((store) => store.colorScheme);
const toggleNightMode = useThemeStore((store) => store.toggleColorScheme);
@@ -463,8 +465,9 @@ function NavigationMenu(props: NavigationMenuProps) {
id={settings.id}
isTablet={isTablet}
key={settings.path}
title={settings.title}
icon={settings.icon}
title={profile?.fullName || settings.title}
icon={profile?.fullName ? User : settings.icon}
image={profile?.fullName ? profile?.profilePicture : undefined}
onClick={() => {
if (!isMobile && location === settings.path)
return toggleNavigationContainer();

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Button, Flex, FlexProps, Text } from "@theme-ui/components";
import { Button, Flex, FlexProps, Image, Text } from "@theme-ui/components";
import { useStore as useAppStore } from "../../stores/app-store";
import { Menu } from "../../hooks/use-menu";
import useMobile from "../../hooks/use-mobile";
@@ -29,7 +29,8 @@ import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
type NavigationItemProps = {
icon: Icon;
icon?: Icon;
image?: string;
color?: SchemeColors;
title: string;
isTablet?: boolean;
@@ -49,6 +50,7 @@ function NavigationItem(
) {
const {
icon: Icon,
image,
color,
title,
isLoading,
@@ -121,11 +123,15 @@ function NavigationItem(
if (onClick) onClick();
}}
>
<Icon
size={isTablet ? 16 : 15}
color={color || (selected ? "icon-selected" : "icon")}
rotate={isLoading}
/>
{image ? (
<Image src={image} sx={{ borderRadius: 50, size: 20 }} />
) : Icon ? (
<Icon
size={isTablet ? 16 : 15}
color={color || (selected ? "icon-selected" : "icon")}
rotate={isLoading}
/>
) : null}
{isShortcut && (
<Shortcut
size={8}

View File

@@ -0,0 +1,166 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Perform } from "../common/dialog-controller";
import Dialog from "../components/dialog";
import { Profile } from "@notesnook/core";
import Field from "../components/field";
import AvatarEditor from "react-avatar-editor";
import { Avatar, Button, Flex, Slider } from "@theme-ui/components";
import { User } from "../components/icons";
import { useRef, useState } from "react";
import { showFilePicker } from "../utils/file-picker";
import { db } from "../common/db";
import { useStore as useUserStore } from "../stores/user-store";
import { showToast } from "../utils/toast";
export type EditProfileDialogProps = {
onClose: Perform;
profile?: Profile;
};
export default function EditProfileDialog(props: EditProfileDialogProps) {
const { profile } = props;
const [profilePicture, setProfilePicture] = useState<
File | string | undefined
>(profile?.profilePicture);
const profileRef = useRef<AvatarEditor>(null);
const [fullName, setFullName] = useState<string | undefined>(
profile?.fullName
);
const [scale, setScale] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [clearProfilePicture, setClearProfilePicture] = useState(false);
return (
<Dialog
isOpen={true}
title={"Edit profile"}
description="Your profile data is stored 100% end-to-end encrypted."
onClose={() => props.onClose(false)}
positiveButton={{
loading: isLoading,
disabled: isLoading,
text: "Save",
onClick: async () => {
setIsLoading(true);
try {
await db.user.setProfile({
fullName,
profilePicture: profileRef.current
? profileRef.current
.getImageScaledToCanvas()
.toDataURL("image/jpeg", 1)
: clearProfilePicture
? undefined
: profile?.profilePicture
});
await useUserStore.getState().refreshUser();
showToast("success", "Profile updated!");
props.onClose(true);
} catch (e) {
console.error(e);
showToast("error", (e as Error).message);
} finally {
setIsLoading(false);
}
}
}}
width={400}
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
>
<Flex sx={{ gap: 2, mt: 2 }}>
<Flex sx={{ flexDirection: "column" }}>
{profilePicture ? (
<AvatarEditor
ref={profileRef}
image={profilePicture}
width={150}
height={150}
border={0}
color={[255, 255, 255]}
borderRadius={100}
scale={scale}
style={{
width: 150,
height: 150
}}
/>
) : (
<Flex
variant="columnCenter"
sx={{
bg: "shade",
mr: 2,
size: 150,
borderRadius: 200,
alignSelf: "center"
}}
>
<User size={60} />
</Flex>
)}
<Flex sx={{ gap: 1, alignItems: "center", mt: 2 }}>
<Button
sx={{ flex: 1 }}
variant="secondary"
onClick={async () =>
setProfilePicture(
await showFilePicker({ acceptedFileTypes: "image/*" })
)
}
>
{profilePicture ? "Change" : "Set picture"}
</Button>
{profilePicture ? (
<Button
sx={{ flex: 1 }}
variant="secondary"
onClick={async () => {
if (profile?.profilePicture) setClearProfilePicture(true);
setProfilePicture(undefined);
setScale(1);
}}
>
{profile?.profilePicture ? "Clear" : "Reset"}
</Button>
) : null}
</Flex>
{profilePicture ? (
<Slider
max={5}
min={1}
step={0.1}
value={scale}
sx={{ color: "accent" }}
onChange={(e) => setScale(e.target.valueAsNumber)}
/>
) : null}
</Flex>
<Field
label="Full name"
maxLength={200}
value={fullName}
autoFocus
onChange={(e) => setFullName(e.target.value)}
/>
</Flex>
</Dialog>
);
}

View File

@@ -56,6 +56,7 @@ const BackupPasswordDialog = React.lazy(
() => import("./backup-password-dialog")
);
const CreateColorDialog = React.lazy(() => import("./create-color-dialog"));
const EditProfileDialog = React.lazy(() => import("./edit-profile-dialog"));
export const Dialogs = {
AddNotebookDialog,
@@ -85,5 +86,6 @@ export const Dialogs = {
SettingsDialog,
ThemeDetailsDialog,
BackupPasswordDialog,
CreateColorDialog
CreateColorDialog,
EditProfileDialog
};

View File

@@ -17,17 +17,19 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Flex, Text } from "@theme-ui/components";
import { User } from "../../../components/icons";
import { Button, Flex, Image, Text } from "@theme-ui/components";
import { Edit, User } from "../../../components/icons";
import { useStore as useUserStore } from "../../../stores/user-store";
import { getObjectIdTimestamp } from "@notesnook/core/dist/utils/object-id";
import { getFormattedDate } from "@notesnook/common";
import { SUBSCRIPTION_STATUS } from "../../../common/constants";
import dayjs from "dayjs";
import { useMemo } from "react";
import { showEditProfileDialog } from "../../../common/dialog-controller";
export function UserProfile() {
const user = useUserStore((store) => store.user);
const profile = useUserStore((store) => store.profile);
const {
isTrial,
@@ -88,42 +90,82 @@ export function UserProfile() {
sx={{
borderRadius: "default",
alignItems: "center",
justifyContent: "space-between",
bg: "var(--background-secondary)",
p: 2,
mb: 4
}}
>
<Flex
variant="columnCenter"
sx={{
bg: "shade",
mr: 2,
size: 60,
borderRadius: 80
}}
>
<User size={30} />
</Flex>
<Flex sx={{ flexDirection: "column" }}>
<Text
variant="subBody"
<Flex sx={{ alignItems: "center" }}>
<Flex
variant="columnCenter"
sx={{
color: "accent"
bg: "shade",
mr: 2,
size: 60,
borderRadius: 80,
overflow: "hidden"
}}
>
{remainingDays > 0 && (isPro || isProCancelled)
? `PRO`
: remainingDays > 0 && isTrial
? "TRIAL"
: isBeta
? "BETA TESTER"
: "BASIC"}
</Text>
<Text variant={"title"}>{user.email}</Text>
<Text variant={"subBody"}>
Member since {getFormattedDate(getObjectIdTimestamp(user.id), "date")}
</Text>
{profile?.profilePicture ? (
<Image
sx={{ width: "100%", height: "100%", objectFit: "contain" }}
src={profile.profilePicture}
/>
) : (
<User size={30} />
)}
</Flex>
<Flex sx={{ flexDirection: "column" }}>
<Text
variant="subBody"
sx={{
color: "accent"
}}
>
{remainingDays > 0 && (isPro || isProCancelled)
? `PRO`
: remainingDays > 0 && isTrial
? "TRIAL"
: isBeta
? "BETA TESTER"
: "BASIC"}
</Text>
{profile?.fullName ? (
<>
<Text variant={"title"}>{profile?.fullName}</Text>
<Text variant={"subBody"}>
{user.email} Member since{" "}
{getFormattedDate(getObjectIdTimestamp(user.id), "date")}
</Text>
</>
) : (
<>
<Text variant={"title"}>{user.email}</Text>
<Text variant={"subBody"}>
Member since
{getFormattedDate(getObjectIdTimestamp(user.id), "date")}
</Text>
</>
)}
</Flex>
</Flex>
<Button
variant="icon"
sx={{
borderRadius: 50,
p: 0,
m: 0,
width: 30,
height: 30,
alignSelf: "end"
}}
title="Edit profile"
onClick={() => showEditProfileDialog(profile)}
>
<Edit size={18} />
</Button>
</Flex>
);
}

View File

@@ -31,7 +31,7 @@ import { hashNavigate } from "../navigation";
import { isUserPremium } from "../hooks/use-is-user-premium";
import { SUBSCRIPTION_STATUS } from "../common/constants";
import { ANALYTICS_EVENTS, trackEvent } from "../utils/analytics";
import { User } from "@notesnook/core/dist/api/user-manager";
import { AuthenticatorType, Profile, User } from "@notesnook/core";
class UserStore extends BaseStore<UserStore> {
isLoggedIn?: boolean;
@@ -39,6 +39,7 @@ class UserStore extends BaseStore<UserStore> {
isSigningIn = false;
user?: User = undefined;
profile?: Profile;
counter = 0;
init = () => {
@@ -47,32 +48,35 @@ class UserStore extends BaseStore<UserStore> {
window.location.replace("/sessionexpired");
});
db.user.getUser().then(async (user) => {
db.user.getUser().then((user) => {
if (!user) {
this.set((state) => {
state.isLoggedIn = false;
});
this.set({ isLoggedIn: false });
return;
}
this.set((state) => {
state.user = user;
state.isLoggedIn = true;
this.set({
user,
isLoggedIn: true
});
if (Config.get("sessionExpired")) EV.publish(EVENTS.userSessionExpired);
});
db.user.getProfile().then((profile) => this.set({ profile }));
if (Config.get("sessionExpired")) return;
return db.user.fetchUser().then(async (user) => {
if (!user) return false;
EV.remove(EVENTS.userSubscriptionUpdated, EVENTS.userEmailConfirmed);
const profile = await db.user.getProfile();
this.set((state) => {
state.user = user;
state.isLoggedIn = true;
this.set({
profile,
user,
isLoggedIn: true
});
EV.remove(EVENTS.userSubscriptionUpdated, EVENTS.userEmailConfirmed);
EV.subscribe(EVENTS.userSubscriptionUpdated, (subscription) => {
const wasUserPremium = isUserPremium();
this.set((state) => {
@@ -107,18 +111,29 @@ class UserStore extends BaseStore<UserStore> {
refreshUser = async () => {
return db.user.fetchUser().then(async (user) => {
this.set((state) => (state.user = user));
const profile = await db.user.getProfile();
this.set({ user, profile });
});
};
login = async (form, skipInit = false, sessionExpired = false) => {
login = async (
form:
| { email: string }
| { email: string; password: string }
| { code: string; method: AuthenticatorType },
skipInit = false,
sessionExpired = false
) => {
this.set((state) => (state.isLoggingIn = true));
const { email, password, code, method } = form;
try {
if (code) {
if ("email" in form && !("password" in form)) {
return await db.user.authenticateEmail(form.email);
} else if ("code" in form) {
const { code, method } = form;
return await db.user.authenticateMultiFactorCode(code, method);
} else if (password) {
} else if ("password" in form) {
const { email, password } = form;
await db.user.authenticatePassword(
email,
password,
@@ -129,15 +144,13 @@ class UserStore extends BaseStore<UserStore> {
if (skipInit) return true;
return this.init();
} else if (email) {
return await db.user.authenticateEmail(email);
}
} finally {
this.set((state) => (state.isLoggingIn = false));
}
};
signup = (form) => {
signup = (form: { email: string; password: string }) => {
this.set((state) => (state.isSigningIn = true));
return db.user
.signup(form.email.toLowerCase(), form.password)