mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
61 Commits
3.4.5-andr
...
feat/ui-ov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8de433cee1 | ||
|
|
ee1c88b7cf | ||
|
|
d6a5e27eb4 | ||
|
|
318f500fc7 | ||
|
|
f2616468fa | ||
|
|
dd3489272c | ||
|
|
3b1dd47fc8 | ||
|
|
386155caa6 | ||
|
|
2260bc774a | ||
|
|
de491c0015 | ||
|
|
5fadb95ddd | ||
|
|
9c5cf1a5ec | ||
|
|
03af07ce33 | ||
|
|
3112c6466f | ||
|
|
2c0f7b8f8f | ||
|
|
5bb4dfedd7 | ||
|
|
a6a7c60035 | ||
|
|
ce44bda0bc | ||
|
|
a934277c98 | ||
|
|
4e32106aa9 | ||
|
|
1d5119dde9 | ||
|
|
1e73a511c1 | ||
|
|
8fee146a32 | ||
|
|
ff3570bae8 | ||
|
|
7ffaab036b | ||
|
|
73d2fe1271 | ||
|
|
b6ace863b3 | ||
|
|
912fa3fe49 | ||
|
|
394c12856b | ||
|
|
1473a70681 | ||
|
|
6e4d5490bc | ||
|
|
1a864743d0 | ||
|
|
e87a9297ce | ||
|
|
72e2010074 | ||
|
|
5d151045cd | ||
|
|
60d7208696 | ||
|
|
882a667269 | ||
|
|
7583e16152 | ||
|
|
02f537e296 | ||
|
|
a462e5a10e | ||
|
|
800c1ccd4d | ||
|
|
33880c682a | ||
|
|
a2cd14ffc8 | ||
|
|
7ba5e72f64 | ||
|
|
0e5b216773 | ||
|
|
ee43a020b2 | ||
|
|
364225b34b | ||
|
|
cb862e7293 | ||
|
|
247a2824e8 | ||
|
|
51dceb9715 | ||
|
|
a0ba1fb1d2 | ||
|
|
7ae1cc85ac | ||
|
|
67d25797d6 | ||
|
|
b3b00fc7cf | ||
|
|
ba253db226 | ||
|
|
9db7795e31 | ||
|
|
526487c37e | ||
|
|
37e7d44587 | ||
|
|
2e09a691e7 | ||
|
|
1c8e43d085 | ||
|
|
6ee2e777c2 |
51
agent.md
Normal file
51
agent.md
Normal file
@@ -0,0 +1,51 @@
|
||||
## Notesnook Design Agent
|
||||
|
||||
### Purpose
|
||||
|
||||
- Implement Figma designs from user-provided links into the Notesnook app with high visual fidelity.
|
||||
- Use related components and files provided by the user as the source of truth for local patterns.
|
||||
- Preserve behavior, navigation, and existing data flow while updating UI.
|
||||
|
||||
### When To Use This Agent
|
||||
|
||||
- Use this agent when the task is design-to-code for app UI.
|
||||
- Prefer this agent over a general coding agent when the user provides a Figma link and asks for redesign implementation.
|
||||
|
||||
### Scope
|
||||
|
||||
- Primary focus: apps/mobile.
|
||||
- Secondary scope: shared UI dependencies needed for the design update (tokens, icons, strings, and generated assets).
|
||||
- Do not perform unrelated refactors.
|
||||
|
||||
### Tool Preferences
|
||||
|
||||
- Prefer Figma design-context tools to extract layout, spacing rhythm, typography intent, and asset references from the exact node.
|
||||
- Prefer direct workspace edits and targeted validation for touched files.
|
||||
- Avoid introducing new UI frameworks or web-specific styling approaches.
|
||||
|
||||
### Repo Conventions
|
||||
|
||||
- Use theme and token systems first: useThemeColors, spacing/radius constants, and shared typography primitives.
|
||||
- Reuse existing UI primitives and patterns before creating new abstractions.
|
||||
- Keep dark and light mode compatibility by default.
|
||||
- Keep changes minimal, isolated, and review-friendly.
|
||||
|
||||
### Icon Conventions
|
||||
|
||||
- Add new icon SVGs to packages/icons/svgs with lowercase kebab-case names.
|
||||
- Regenerate icon assets from apps/mobile using npx react-native-nano-icons.
|
||||
- Use generated icon names through AppIcon with icon family notesnook where applicable.
|
||||
|
||||
### Figma To Code Workflow
|
||||
|
||||
1. Read the exact Figma node from the provided link.
|
||||
2. Compare against existing repo patterns and referenced components.
|
||||
3. Implement with React Native conventions already used in the app.
|
||||
4. Preserve behavior and interactions unless explicitly changed.
|
||||
5. Validate compile and lint state for edited files.
|
||||
|
||||
### Output Expectations
|
||||
|
||||
- Provide a concise summary of what changed.
|
||||
- List modified files and any generated artifacts.
|
||||
- Call out follow-up steps only when needed.
|
||||
9
apps/mobile/.nanoicons.json
Normal file
9
apps/mobile/.nanoicons.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"iconSets": [
|
||||
{
|
||||
"inputDir": "../../packages/icons/svgs",
|
||||
"fontFamily": "notesnook-icons",
|
||||
"outputDir": "./fonts"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import { Toast } from "./components/toast";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { NotePreviewConfigure } from "./screens/note-preview-configure";
|
||||
import { RootNavigation } from "./navigation/navigation-stack";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
import { themeTrpcClient } from "./screens/settings/components/theme-selector";
|
||||
import Notifications from "./services/notifications";
|
||||
import SettingsService from "./services/settings";
|
||||
import { TipManager } from "./services/tip-manager";
|
||||
@@ -44,6 +44,7 @@ import { useUserStore } from "./stores/use-user-store";
|
||||
import RNBootSplash from "react-native-bootsplash";
|
||||
import AppLocked from "./components/app-lock";
|
||||
import { useSettingStore } from "./stores/use-setting-store";
|
||||
import { THEME_DARK, THEME_LIGHT } from "./common/design/theme";
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
@@ -120,30 +121,30 @@ export const withTheme = (
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (!currentTheme) return;
|
||||
themeTrpcClient.updateTheme
|
||||
.query({
|
||||
version: currentTheme.version,
|
||||
compatibilityVersion: THEME_COMPATIBILITY_VERSION,
|
||||
id: currentTheme.id
|
||||
})
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.setState({
|
||||
darkTheme: theme
|
||||
})
|
||||
: useThemeStore.setState({
|
||||
lightTheme: theme
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}, 1000);
|
||||
// setTimeout(() => {
|
||||
// const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
// if (!currentTheme) return;
|
||||
// themeTrpcClient.updateTheme
|
||||
// .query({
|
||||
// version: currentTheme.version,
|
||||
// compatibilityVersion: THEME_COMPATIBILITY_VERSION,
|
||||
// id: currentTheme.id
|
||||
// })
|
||||
// .then((theme) => {
|
||||
// if (theme) {
|
||||
// theme.colorScheme === "dark"
|
||||
// ? useThemeStore.setState({
|
||||
// darkTheme: theme
|
||||
// })
|
||||
// : useThemeStore.setState({
|
||||
// lightTheme: theme
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .catch(() => {
|
||||
// /* empty */
|
||||
// });
|
||||
// }, 1000);
|
||||
|
||||
const listener = Appearance.addChangeListener(({ colorScheme }) => {
|
||||
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
|
||||
@@ -159,11 +160,11 @@ export const withTheme = (
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
const nextTheme = colorScheme === "dark" ? THEME_DARK : THEME_LIGHT;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
.setTheme(colorScheme === "dark" ? THEME_DARK : THEME_LIGHT);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
}, [colorScheme, darkTheme, lightTheme]);
|
||||
|
||||
File diff suppressed because one or more lines are too long
56
apps/mobile/app/common/design/font.ts
Normal file
56
apps/mobile/app/common/design/font.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
export const FontSizes = {
|
||||
XXS: 10,
|
||||
XS: 12,
|
||||
SM: 14,
|
||||
MD: 16,
|
||||
LG: 18,
|
||||
XL: 20,
|
||||
XXL: 32
|
||||
};
|
||||
|
||||
export const FontFamily = {
|
||||
REGULAR: "Inter-Regular", // 400
|
||||
MEDIUM: "Inter-Medium", // 500
|
||||
SEMI_BOLD: "Inter-SemiBold", // 600
|
||||
BOLD: "Inter-Bold" // 700
|
||||
};
|
||||
|
||||
const LineHeightMultipliers = {
|
||||
"100%": 1,
|
||||
"110%": 1.1,
|
||||
"120%": 1.2,
|
||||
"130%": 1.3,
|
||||
"140%": 1.4,
|
||||
"150%": 1.5
|
||||
};
|
||||
export type LineHeightVariants =
|
||||
| "100%"
|
||||
| "110%"
|
||||
| "120%"
|
||||
| "130%"
|
||||
| "140%"
|
||||
| "150%";
|
||||
export const getLineHeight = (
|
||||
fontSize: keyof typeof FontSizes,
|
||||
type: LineHeightVariants
|
||||
) => {
|
||||
return FontSizes[fontSize] * LineHeightMultipliers[type];
|
||||
};
|
||||
39
apps/mobile/app/common/design/spacing.ts
Normal file
39
apps/mobile/app/common/design/spacing.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
export const Spacing = {
|
||||
LEVEL_0: 4,
|
||||
LEVEL_1: 8,
|
||||
LEVEL_2: 12,
|
||||
LEVEL_3: 16,
|
||||
LEVEL_4: 20,
|
||||
LEVEL_5: 24,
|
||||
LEVEL_6: 28,
|
||||
LEVEL_7: 32,
|
||||
LEVEL_8: 40
|
||||
};
|
||||
|
||||
export const Radius = {
|
||||
XXS: 4,
|
||||
XS: 8,
|
||||
S: 12,
|
||||
MD: 16,
|
||||
LG: 20,
|
||||
LG_2: 32,
|
||||
XXL: 40
|
||||
};
|
||||
283
apps/mobile/app/common/design/theme.ts
Normal file
283
apps/mobile/app/common/design/theme.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
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 { ThemeDefinition } from "@notesnook/theme";
|
||||
|
||||
export const THEME_LIGHT: any = {
|
||||
$schema:
|
||||
"https://raw.githubusercontent.com/streetwriters/notesnook-themes/main/schemas/v1.schema.json",
|
||||
name: "Notesnook Light",
|
||||
id: "default-light",
|
||||
version: 2.1,
|
||||
license: "GPL-3.0-or-later",
|
||||
homepage: "https://notesnook.com",
|
||||
description: "The default light theme for Notesnook app.",
|
||||
colorScheme: "light",
|
||||
codeBlockCSS: ``,
|
||||
compatibilityVersion: 1,
|
||||
authors: [
|
||||
{
|
||||
name: "Streetwriters",
|
||||
email: "support@streetwriters.co",
|
||||
url: "https://streetwriters.co"
|
||||
}
|
||||
],
|
||||
scopes: {
|
||||
base: {
|
||||
primary: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#666666",
|
||||
background: "#ffffff",
|
||||
border: "#E2E2E2",
|
||||
heading: "#181818",
|
||||
icon: "#181818",
|
||||
separator: "#F0F0F0",
|
||||
placeholder: "#858585",
|
||||
hover: "#eee",
|
||||
backdrop: "#0000001a",
|
||||
buttonForeground: "#FFFFFF"
|
||||
},
|
||||
secondary: {
|
||||
accent: "#0D9E46",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#858585",
|
||||
background: "#F7F7F7",
|
||||
border: "#EFEFEF",
|
||||
heading: "#4B4B4B",
|
||||
icon: "#666666",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#999999",
|
||||
hover: "#dbdbdb",
|
||||
backdrop: "#0000001a",
|
||||
buttonForeground: "#181818"
|
||||
},
|
||||
tertiary: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#777",
|
||||
background: "#ECECEC",
|
||||
border: "#E8E8E8",
|
||||
heading: "#666",
|
||||
icon: "#777",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#a9a9a9",
|
||||
hover: "#dbdbdb",
|
||||
backdrop: "#0000001a"
|
||||
},
|
||||
disabled: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#A6A6A6",
|
||||
background: "#ffffff",
|
||||
border: "#E8E8E8",
|
||||
heading: "#212121",
|
||||
icon: "#DADADA",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#a9a9a9",
|
||||
hover: "#00000010",
|
||||
backdrop: "#0000001a"
|
||||
},
|
||||
selected: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#212121",
|
||||
background: "#00883610",
|
||||
border: "#008836",
|
||||
heading: "#212121",
|
||||
icon: "#181818",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#a9a9a9",
|
||||
hover: "#eee",
|
||||
backdrop: "#0000001a"
|
||||
},
|
||||
error: {
|
||||
accent: "#f54b42",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#FF242E",
|
||||
background: "#FFEDEE",
|
||||
border: "#F7999D",
|
||||
heading: "#f54b42",
|
||||
icon: "#FB2C36",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#a9a9a9",
|
||||
hover: "#00000010",
|
||||
backdrop: "#0000001a"
|
||||
},
|
||||
success: {
|
||||
accent: "#4F8A10",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#4F8A10",
|
||||
background: "#85ce68",
|
||||
border: "#E8E8E8",
|
||||
heading: "#4F8A10",
|
||||
icon: "#4F8A10",
|
||||
separator: "#E8E8E8",
|
||||
placeholder: "#a9a9a9",
|
||||
hover: "#00000010",
|
||||
backdrop: "#0000001a"
|
||||
}
|
||||
},
|
||||
statusBar: {
|
||||
primary: { paragraph: "#5E5E5E", background: "#f7f7f7" }
|
||||
},
|
||||
navigationMenu: {
|
||||
primary: {
|
||||
background: "#f7f7f7",
|
||||
paragraph: "#666",
|
||||
icon: "#666",
|
||||
hover: "#eee"
|
||||
},
|
||||
secondary: { background: "#EEEEEE" },
|
||||
selected: {
|
||||
background: "#00883610",
|
||||
hover: "#eee"
|
||||
}
|
||||
},
|
||||
contextMenu: { primary: { background: "#f7f7f7" } },
|
||||
editor: { selected: { background: "#e1e1e1" } },
|
||||
sheet: { selected: { paragraph: "#008836" } },
|
||||
list: {
|
||||
secondary: {
|
||||
heading: "#A6A6A6"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const THEME_DARK: any = {
|
||||
name: "Notesnook Dark",
|
||||
id: "default-dark",
|
||||
version: 2.1,
|
||||
license: "GPL-3.0-or-later",
|
||||
homepage: "https://notesnook.com",
|
||||
description: "The default dark theme for the Notesnook app",
|
||||
colorScheme: "dark",
|
||||
codeBlockCSS: ``,
|
||||
compatibilityVersion: 1,
|
||||
authors: [
|
||||
{
|
||||
name: "Streetwriters",
|
||||
email: "support@streetwriters.co",
|
||||
url: "https://streetwriters.co"
|
||||
}
|
||||
],
|
||||
scopes: {
|
||||
base: {
|
||||
primary: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#F2F2F2",
|
||||
background: "#181818",
|
||||
border: "#2A2A2A",
|
||||
heading: "#FFFFFF",
|
||||
icon: "#FFFFFF",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#2b2b2b",
|
||||
backdrop: "#35353580",
|
||||
buttonForeground: "#FFFFFF"
|
||||
},
|
||||
secondary: {
|
||||
accent: "#0D9E46",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#A1A1A1",
|
||||
background: "#252525",
|
||||
border: "#0A5729",
|
||||
heading: "#C0BFBF",
|
||||
icon: "#808080",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580",
|
||||
buttonForeground: "#BFBFBF"
|
||||
},
|
||||
tertiary: {
|
||||
accent: "#0D9E46",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#818589",
|
||||
background: "#2C2C2C",
|
||||
border: "#2b2b2b",
|
||||
heading: "#808080",
|
||||
icon: "#808080",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580"
|
||||
},
|
||||
disabled: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#505050",
|
||||
background: "#ffffff",
|
||||
border: "#383838",
|
||||
heading: "#212121",
|
||||
icon: "#FFFFFF",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580"
|
||||
},
|
||||
selected: {
|
||||
accent: "#008836",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#FBFBFB",
|
||||
background: "#494949",
|
||||
border: "#008836",
|
||||
heading: "#e3e3e3",
|
||||
icon: "#008836",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580"
|
||||
},
|
||||
error: {
|
||||
accent: "#f54b42",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#f54b42",
|
||||
background: "#250b0a",
|
||||
border: "#F7999D",
|
||||
heading: "#f54b42",
|
||||
icon: "#f54b42",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580"
|
||||
},
|
||||
success: {
|
||||
accent: "#4F8A10",
|
||||
accentForeground: "#ffffff",
|
||||
paragraph: "#4F8A10",
|
||||
background: "#132204",
|
||||
border: "#383838",
|
||||
heading: "#4F8A10",
|
||||
icon: "#4F8A10",
|
||||
separator: "#383838",
|
||||
placeholder: "#404040",
|
||||
hover: "#ffffff10",
|
||||
backdrop: "#35353580"
|
||||
}
|
||||
},
|
||||
list: { selected: { background: "#202020" } },
|
||||
contextMenu: { primary: { background: "#202020" } },
|
||||
sheet: { selected: { paragraph: "#008836" } }
|
||||
},
|
||||
$schema:
|
||||
"https://raw.githubusercontent.com/streetwriters/notesnook-themes/main/schemas/v1.schema.json"
|
||||
};
|
||||
@@ -55,6 +55,7 @@ import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
|
||||
|
||||
const getUser = () => {
|
||||
const user = MMKV.getString("user");
|
||||
@@ -96,9 +97,6 @@ const AppLocked = () => {
|
||||
const lastAppState = useRef<AppStateStatus>(appState);
|
||||
const biometricUnlockAwaitingUserInput = useRef(false);
|
||||
const { height } = useWindowDimensions();
|
||||
const keyboardType = useSettingStore(
|
||||
(state) => state.settings.applockKeyboardType
|
||||
);
|
||||
const appLockHasPasswordSecurity = useSettingStore(
|
||||
(state) => state.settings.appLockHasPasswordSecurity
|
||||
);
|
||||
@@ -157,8 +155,7 @@ const AppLocked = () => {
|
||||
if (!appLockHasPasswordSecurity) {
|
||||
await setAppLockVerificationCipher(password.current);
|
||||
SettingsService.set({
|
||||
appLockHasPasswordSecurity: true,
|
||||
applockKeyboardType: "default"
|
||||
appLockHasPasswordSecurity: true
|
||||
});
|
||||
DatabaseLogger.info("App lock migrated to password security");
|
||||
}
|
||||
@@ -167,7 +164,7 @@ const AppLocked = () => {
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: strings.invalid(keyboardType),
|
||||
heading: strings.invalid("password"),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -290,9 +287,7 @@ const AppLocked = () => {
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
secureTextEntry
|
||||
keyboardType={
|
||||
appLockHasPasswordSecurity ? keyboardType : "default"
|
||||
}
|
||||
keyboardType={"default"}
|
||||
onLayout={async () => {
|
||||
if (
|
||||
!biometricsAuthEnabled ||
|
||||
@@ -303,13 +298,7 @@ const AppLocked = () => {
|
||||
}, 32);
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
appLockHasPasswordSecurity
|
||||
? keyboardType === "numeric"
|
||||
? strings.enterApplockPassword()
|
||||
: strings.enterApplockPin()
|
||||
: strings.enterAccountPassword()
|
||||
}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
|
||||
@@ -34,7 +34,7 @@ import create from "zustand";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import { AttachmentGroupProgress } from "../../screens/settings/attachment-group-progress";
|
||||
import { AttachmentGroupProgress } from "../../screens/settings/components/attachment-group-progress";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
|
||||
@@ -23,10 +23,9 @@ import React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import BackupService from "../../services/backup";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Dialog } from "../dialog";
|
||||
@@ -36,6 +35,8 @@ import FormInput, { createFormRef, validators } from "../ui/input/form-input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { TextInput } from "react-native-gesture-handler";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -92,7 +93,7 @@ export const ChangePassword = () => {
|
||||
});
|
||||
setLoading(false);
|
||||
Navigation.goBack();
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
RecoveryKeySheet.present();
|
||||
} catch (e) {
|
||||
const message = (e as Error).message;
|
||||
setLoading(false);
|
||||
@@ -109,12 +110,15 @@ export const ChangePassword = () => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: DefaultAppStyles.GAP
|
||||
paddingTop: Spacing.LEVEL_0,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<FormInput
|
||||
name="oldPassword"
|
||||
label={strings.oldPassword()}
|
||||
formRef={formRef}
|
||||
fwdRef={oldPasswordInputRef}
|
||||
loading={loading}
|
||||
@@ -125,7 +129,7 @@ export const ChangePassword = () => {
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.currentPassword()}
|
||||
placeholder={"•••••••••"}
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
@@ -133,6 +137,7 @@ export const ChangePassword = () => {
|
||||
|
||||
<FormInput
|
||||
name="password"
|
||||
label={strings.newPassword()}
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
loading={loading}
|
||||
@@ -143,7 +148,28 @@ export const ChangePassword = () => {
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.newPassword()}
|
||||
placeholder={"•••••••••"}
|
||||
onSubmitEditing={() => {
|
||||
changePassword();
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
name="confirmPassword"
|
||||
label={strings.confirmPassword()}
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
loading={loading}
|
||||
validators={[
|
||||
validators.matchField("password", strings.confirmPasswordRequired())
|
||||
]}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={"•••••••••"}
|
||||
onSubmitEditing={() => {
|
||||
changePassword();
|
||||
}}
|
||||
@@ -168,15 +194,14 @@ export const ChangePassword = () => {
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Notice text={strings.changePasswordNotice()} type="alert" />
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
<Notice text={strings.changePasswordNotice2()} type="alert" />
|
||||
<Notice
|
||||
text={strings.changePasswordNotice()}
|
||||
type="information"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
loading={loading}
|
||||
@@ -184,6 +209,23 @@ export const ChangePassword = () => {
|
||||
type="accent"
|
||||
title={loading ? null : strings.changePasswordConfirm()}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_1,
|
||||
alignSelf: "center",
|
||||
marginTop: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
size={15}
|
||||
name="shield-check"
|
||||
iconFamily="notesnook"
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<Paragraph>{strings.yourSecurityIsPriority()}</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,9 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth } from "./common";
|
||||
import { AuthParams } from "../../stores/use-navigation-store";
|
||||
import { ProgressPills } from "../intro/progress-pills";
|
||||
import { FontSizes } from "../../common/design/font";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const route = useRoute();
|
||||
@@ -37,19 +40,22 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: !props.welcome ? "space-between" : "flex-end"
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{props.welcome ? null : (
|
||||
{props.welcome ? (
|
||||
<ProgressPills activePillIndex={1} />
|
||||
) : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
name="arrow-back"
|
||||
iconFamily="notesnook"
|
||||
onPress={() => {
|
||||
hideAuth((route.params as AuthParams)?.context);
|
||||
}}
|
||||
size={24}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
@@ -60,16 +66,16 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
iconSize={24}
|
||||
fontSize={FontSizes.SM}
|
||||
type="plain"
|
||||
fontFamily="REGULAR"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
paddingRight: 0,
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -17,15 +17,15 @@ 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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { NavigationProps } from "../../services/navigation";
|
||||
import { Toast } from "../toast";
|
||||
import { AuthMode, initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { NavigationProps } from "../../services/navigation";
|
||||
|
||||
const Auth = ({ navigation, route }: NavigationProps<"Auth">) => {
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(
|
||||
|
||||
@@ -21,8 +21,10 @@ import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { RouteProp, useRoute } from "@react-navigation/native";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
@@ -33,24 +35,24 @@ import { RouteParams } from "../../stores/use-navigation-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import { ProgressPills } from "../intro/progress-pills";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { AuthHeader } from "./header";
|
||||
import TwoFactorVerification from "./two-factor";
|
||||
import { useLogin } from "./use-login";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
mfaAuth: 2,
|
||||
passwordAuth: 3
|
||||
mfaAuth: 2
|
||||
};
|
||||
|
||||
export const Login = ({
|
||||
@@ -68,8 +70,9 @@ export const Login = ({
|
||||
passwordInputRef,
|
||||
loading,
|
||||
setLoading,
|
||||
login,
|
||||
error,
|
||||
login,
|
||||
mfaData,
|
||||
formRef
|
||||
} = useLogin(async () => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
@@ -91,8 +94,6 @@ export const Login = ({
|
||||
Progress.present();
|
||||
}
|
||||
});
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
|
||||
const onContinue = () => {
|
||||
login();
|
||||
@@ -114,249 +115,257 @@ export const Login = ({
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
<Dialog context="two_factor_verify" />
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
marginTop: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<ProgressPills
|
||||
count={2}
|
||||
activePillIndex={step === LoginSteps.emailAuth ? 0 : 1}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
minHeight: "99%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={true}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
{step === LoginSteps.emailAuth ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : undefined,
|
||||
borderColor: isTablet ? colors.primary.border : undefined,
|
||||
borderRadius: isTablet ? 20 : undefined,
|
||||
marginTop: isTablet ? 50 : undefined,
|
||||
width: !isTablet ? undefined : "50%",
|
||||
minHeight: height * 0.4
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_6
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
extraBold
|
||||
size={AppFontSize.xxl}
|
||||
fontSize="XL"
|
||||
>
|
||||
{strings.loginToYourAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
testID="input.email"
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
marginBottom={0}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
placeholder={strings.email()}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
onContinue();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
onContinue();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
presentSheet({
|
||||
component: (
|
||||
<ForgotPassword
|
||||
userEmail={formRef.current.getValue("email")}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
onContinue();
|
||||
>
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
testID="input.email"
|
||||
label={strings.email()}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
marginBottom={0}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
placeholder={strings.email()}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
fontSize={AppFontSize.sm}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
label={strings.password()}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
onContinue();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading || !formRef.current.getValue("email")) return;
|
||||
presentSheet({
|
||||
component: (
|
||||
<ForgotPassword
|
||||
userEmail={formRef.current.getValue("email")}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}}
|
||||
fontFamily="REGULAR"
|
||||
fontSize={AppFontSize.sm}
|
||||
type="plain"
|
||||
textStyle={{
|
||||
color: colors.secondary.paragraph
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_1,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={strings.cancelLogin()}
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
type="secondaryAccented"
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
fontSize={AppFontSize.sm}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
paddingVertical: 0
|
||||
}}
|
||||
>
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.signUp()}
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
fontSize="SM"
|
||||
style={{ color: colors.primary.accent }}
|
||||
fontFamily="SEMI_BOLD"
|
||||
>
|
||||
{strings.signUp()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_6
|
||||
}}
|
||||
>
|
||||
<TwoFactorVerification
|
||||
onMfaLogin={async (
|
||||
mfa: any,
|
||||
callback: (success: boolean) => void,
|
||||
onerror: (error: Error) => void
|
||||
) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
|
||||
await login();
|
||||
|
||||
if (success) {
|
||||
setLoading(false);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
} else {
|
||||
onerror(e as Error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
mfaInfo={
|
||||
mfaData.current || {
|
||||
primaryMethod: "email",
|
||||
secondaryMethod: "sms",
|
||||
token: ""
|
||||
}
|
||||
}
|
||||
onCancel={() => {
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -45,11 +45,12 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import { useLogin } from "./use-login";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { getObfuscatedEmail } from "../../utils/functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
|
||||
|
||||
export const SessionExpired = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -195,24 +196,22 @@ export const SessionExpired = () => {
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{step === LoginSteps.passwordAuth ? (
|
||||
<FormInput
|
||||
fwdRef={passwordInputRef}
|
||||
formRef={formRef}
|
||||
name="password"
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmitEditing={() => {
|
||||
login();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<FormInput
|
||||
fwdRef={passwordInputRef}
|
||||
formRef={formRef}
|
||||
name="password"
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
onSubmitEditing={() => {
|
||||
login();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
|
||||
@@ -21,12 +21,7 @@ import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { RouteProp, useRoute } from "@react-navigation/native";
|
||||
import React, { useRef, useState } from "react";
|
||||
import {
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import { TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -46,6 +41,29 @@ import { SignupContext } from "./signup-context";
|
||||
import { RouteParams } from "../../stores/use-navigation-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { SETTING_ACCOUNT_SVG } from "../../assets/images/assets";
|
||||
import { ProgressPills } from "../intro/progress-pills";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
|
||||
|
||||
const getSettingAccountSvg = (dark: boolean) => {
|
||||
if (dark) return SETTING_ACCOUNT_SVG;
|
||||
|
||||
return SETTING_ACCOUNT_SVG.replace('stroke="#1F2722"', 'stroke="#E8F4ED"')
|
||||
.replace(
|
||||
'fill="#008836" fill-opacity="0.09" stroke="#233C2D"',
|
||||
'fill="#008836" fill-opacity="0.04" stroke="#E3F1E8"'
|
||||
)
|
||||
.replace(
|
||||
'fill="#008836" fill-opacity="0.09" stroke="#233C2D"',
|
||||
'fill="#008836" fill-opacity="0.05" stroke="#E8F0EC"'
|
||||
)
|
||||
.replace(
|
||||
'fill="#008836" fill-opacity="0.08" stroke="#233C2D"',
|
||||
'fill="#008836" fill-opacity="0.06" stroke="#D3E8DB"'
|
||||
);
|
||||
};
|
||||
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
@@ -61,7 +79,7 @@ export const Signup = ({
|
||||
welcome: boolean;
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
const { colors } = useThemeColors();
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: "",
|
||||
@@ -76,8 +94,6 @@ export const Signup = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
const route = useRoute<RouteProp<RouteParams, "Auth">>();
|
||||
|
||||
const signup = async () => {
|
||||
@@ -89,8 +105,9 @@ export const Signup = ({
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
await db.user.signup(values.email.toLowerCase(), values.password);
|
||||
await sleep(1000);
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
const user = await db.user.getUser();
|
||||
setUser(user);
|
||||
setLastSynced(await db.lastSynced());
|
||||
@@ -133,8 +150,9 @@ export const Signup = ({
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
flex: 1
|
||||
}}
|
||||
bounces={false}
|
||||
nestedScrollEnabled
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
@@ -145,68 +163,24 @@ export const Signup = ({
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%"
|
||||
height: "100%",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_6
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<Heading
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : undefined,
|
||||
borderColor: isTablet ? colors.primary.border : undefined,
|
||||
borderRadius: isTablet ? 20 : undefined,
|
||||
marginTop: isTablet ? 50 : undefined,
|
||||
width: !isTablet ? undefined : "50%",
|
||||
minHeight: height * 0.25
|
||||
paddingBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
fontSize="XL"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{strings.createAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
{strings.createAccount()}
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: DDS.isTab ? 0 : 16,
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
@@ -214,6 +188,7 @@ export const Signup = ({
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
loading={loading}
|
||||
label={strings.email()}
|
||||
testID="input.email"
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
@@ -221,7 +196,7 @@ export const Signup = ({
|
||||
keyboardType="email-address"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
placeholder={strings.email()}
|
||||
placeholder="you@example.com"
|
||||
blurOnSubmit={false}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
@@ -245,9 +220,11 @@ export const Signup = ({
|
||||
autoCapitalize="none"
|
||||
blurOnSubmit={false}
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
label={strings.password()}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
if (formRef.current.validateField("password")) return;
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
@@ -265,8 +242,8 @@ export const Signup = ({
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
label={strings.confirmPassword()}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
validators={[
|
||||
validators.required(strings.confirmPasswordRequired()),
|
||||
validators.matchField(
|
||||
@@ -278,7 +255,17 @@ export const Signup = ({
|
||||
signup();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={!loading ? strings.continue() : null}
|
||||
type="accent"
|
||||
@@ -296,18 +283,21 @@ export const Signup = ({
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
textAlign: "center"
|
||||
}}
|
||||
fontSize="SM"
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{strings.alreadyHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs + 1}
|
||||
fontSize="SM"
|
||||
fontFamily="SEMI_BOLD"
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.login()}
|
||||
@@ -347,18 +337,16 @@ export const Signup = ({
|
||||
marginBottom: 25,
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={AppFontSize.xxs}
|
||||
fontSize="XS"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.signupAgreement[0]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
fontSize="XS"
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontFamily="SEMI_BOLD"
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
@@ -366,31 +354,38 @@ export const Signup = ({
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[2]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
fontSize="XS"
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontFamily="SEMI_BOLD"
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[3]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[4]()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{welcome ? <ProgressPills activePillIndex={2} /> : null}
|
||||
<Loading
|
||||
title={"Setting up your account..."}
|
||||
description="Your account is almost ready, please wait..."
|
||||
title={strings.settingUpYourAccount()}
|
||||
svgSrc={getSettingAccountSvg(isDark)}
|
||||
description={strings.accountAlmostReady()}
|
||||
style={{
|
||||
height: undefined,
|
||||
marginTop: Spacing.LEVEL_6
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</View>
|
||||
)}
|
||||
</SignupContext.Provider>
|
||||
);
|
||||
|
||||
@@ -20,19 +20,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { db } from "../../common/database/index";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import useTimer from "../../hooks/use-timer";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import PinInput from "../ui/pin-input/index";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
@@ -73,11 +72,19 @@ const TwoFactorVerification = ({
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [codeInput, setCodeInput] = useState("");
|
||||
|
||||
const isRecoveryCode = currentMethod.method === "recoveryCode";
|
||||
const codeLength = isRecoveryCode ? 10 : 6;
|
||||
|
||||
const onNext = async () => {
|
||||
if (!code.current || code.current.length < 6) {
|
||||
setError(
|
||||
new Error("Please provide a valid multi-factor authentication code.")
|
||||
new Error(
|
||||
currentMethod.method === "recoveryCode"
|
||||
? strings.provideValidMultiFactorAuthRecoveryCode()
|
||||
: strings.provideValidMultiFactorAuthCode()
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -94,10 +101,7 @@ const TwoFactorVerification = ({
|
||||
method: currentMethod.method,
|
||||
code: code.current
|
||||
},
|
||||
(result) => {
|
||||
if (result) {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
}
|
||||
() => {
|
||||
setLoading(false);
|
||||
},
|
||||
(e) => {
|
||||
@@ -118,12 +122,14 @@ const TwoFactorVerification = ({
|
||||
{
|
||||
id: "sms",
|
||||
title: strings.sendCodeSms(),
|
||||
icon: "message-plus-outline"
|
||||
icon: "chat",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
title: strings.sendCodeEmail(),
|
||||
icon: "email-outline"
|
||||
icon: "envelope-simple",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "app",
|
||||
@@ -133,7 +139,8 @@ const TwoFactorVerification = ({
|
||||
{
|
||||
id: "recoveryCode",
|
||||
title: strings.recoveryCode(),
|
||||
icon: "key"
|
||||
icon: "lock-simple",
|
||||
iconFamily: "notesnook"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -155,6 +162,7 @@ const TwoFactorVerification = ({
|
||||
setSending(false);
|
||||
} catch (e) {
|
||||
setSending(false);
|
||||
console.log("error e");
|
||||
setError(
|
||||
new Error(`Error sending 2FA Code. Tap "Send code" to try again `)
|
||||
);
|
||||
@@ -168,48 +176,56 @@ const TwoFactorVerification = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentMethod.method]);
|
||||
|
||||
useEffect(() => {
|
||||
setCodeInput("");
|
||||
code.current = "";
|
||||
}, [currentMethod.method]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 60
|
||||
}}
|
||||
<View
|
||||
onLayout={() => {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 500);
|
||||
}}
|
||||
style={{
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
paddingHorizontal: currentMethod.method ? 12 : 0,
|
||||
gap: 12
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
width: 70,
|
||||
height: 70
|
||||
width: 50,
|
||||
height: 50,
|
||||
backgroundColor: colors.primary.shade,
|
||||
borderRadius: Radius.XS,
|
||||
marginBottom: Spacing.LEVEL_7
|
||||
}}
|
||||
size={50}
|
||||
name="key"
|
||||
size={25}
|
||||
iconFamily="notesnook"
|
||||
name="shield-check"
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
textAlign: "center",
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{currentMethod.method ? strings["2fa"]() : strings.select2faMethod()}
|
||||
{currentMethod.method === "recoveryCode"
|
||||
? strings.recoveryCodeTitle()
|
||||
: currentMethod.method
|
||||
? strings["2fa"]()
|
||||
: strings.select2faMethod()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
width: "80%",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
@@ -219,61 +235,35 @@ const TwoFactorVerification = ({
|
||||
]?.() || strings.select2faCodeHelpText()
|
||||
: strings.select2faCodeHelpText()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
type={seconds ? "plain" : "transparent"}
|
||||
title={
|
||||
sending
|
||||
? ""
|
||||
: `${
|
||||
seconds
|
||||
? strings.resend2faCode(`${seconds}`)
|
||||
: strings.sendCode()
|
||||
}`
|
||||
}
|
||||
loading={sending}
|
||||
height={30}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{currentMethod.method ? (
|
||||
<>
|
||||
<Input
|
||||
placeholder={
|
||||
currentMethod.method === "recoveryCode"
|
||||
? "xxxxx-xxxxx"
|
||||
: "xxxxxx"
|
||||
}
|
||||
{currentMethod.method ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: Radius.S,
|
||||
width: "100%",
|
||||
gap: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<PinInput
|
||||
testID={"input.totp"}
|
||||
maxLength={
|
||||
currentMethod.method === "recoveryCode" ? undefined : 6
|
||||
}
|
||||
fwdRef={inputRef}
|
||||
textAlign="center"
|
||||
onChangeText={(value) => {
|
||||
setError(undefined);
|
||||
inputRef={inputRef}
|
||||
length={codeLength}
|
||||
value={codeInput}
|
||||
onChangeText={(value: string) => {
|
||||
setCodeInput(value);
|
||||
code.current = value;
|
||||
}}
|
||||
cursorColor={colors.selected.accent}
|
||||
selectionHandleColor={colors.selected.accent}
|
||||
selectionColor={colors.selected.accent}
|
||||
onSubmitEditing={onNext}
|
||||
height={60}
|
||||
marginBottom={0}
|
||||
inputStyle={{
|
||||
fontSize: AppFontSize.lg,
|
||||
textAlign: "center",
|
||||
letterSpacing: 7,
|
||||
width: 250
|
||||
}}
|
||||
keyboardType={
|
||||
currentMethod.method === "recoveryCode" ? "default" : "numeric"
|
||||
}
|
||||
enablesReturnKeyAutomatically
|
||||
containerStyle={{
|
||||
minWidth: "50%"
|
||||
keyboardType={isRecoveryCode ? "web-search" : "number-pad"}
|
||||
showDashAfterDigit={isRecoveryCode ? 5 : undefined}
|
||||
sanitize={(value: string) => {
|
||||
if (isRecoveryCode) {
|
||||
return value.replace(/[^0-9a-zA-Z]/g, "").toUpperCase();
|
||||
}
|
||||
|
||||
return value.replace(/[^0-9]/g, "");
|
||||
}}
|
||||
/>
|
||||
{error ? (
|
||||
@@ -284,7 +274,9 @@ const TwoFactorVerification = ({
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
maxWidth: 250
|
||||
maxWidth: 250,
|
||||
alignSelf: "center",
|
||||
marginTop: -10
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
@@ -296,76 +288,132 @@ const TwoFactorVerification = ({
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
title={loading ? null : strings.next()}
|
||||
type="accent"
|
||||
width={250}
|
||||
loading={loading}
|
||||
onPress={onNext}
|
||||
/>
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="secondaryAccented"
|
||||
onPress={() => {
|
||||
reset();
|
||||
onCancel();
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
width={250}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings["2faCodeSecondaryMethodText"][
|
||||
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
|
||||
]()}
|
||||
type="plain"
|
||||
onPress={onRequestSecondaryMethod}
|
||||
height={30}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{getMethods().map((item) => (
|
||||
<Pressable
|
||||
key={item.title}
|
||||
>
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="secondary"
|
||||
onPress={() => {
|
||||
setCurrentMethod({
|
||||
method: item.id,
|
||||
isPrimary: false
|
||||
});
|
||||
reset();
|
||||
onCancel();
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 0,
|
||||
flexDirection: "row",
|
||||
borderRadius: 0,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start"
|
||||
width: "48%"
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={loading ? null : strings.continue()}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={onNext}
|
||||
style={{
|
||||
width: "48%"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{currentMethod.method === "sms" ||
|
||||
currentMethod.method === "email" ? (
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
type={"plain"}
|
||||
disabled={parseInt(`${seconds}`) !== 0}
|
||||
title={
|
||||
sending
|
||||
? ""
|
||||
: `${
|
||||
seconds
|
||||
? strings.resend2faCode(`${seconds}`)
|
||||
: strings.resendCode()
|
||||
}`
|
||||
}
|
||||
loading={sending}
|
||||
fontSize={AppFontSize.sm}
|
||||
fontFamily="REGULAR"
|
||||
style={{
|
||||
paddingVertical: 0,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={onRequestSecondaryMethod}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
paddingVertical: 0,
|
||||
marginTop: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{strings["2faCodeSecondaryMethodText"][
|
||||
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
|
||||
]()}
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_2,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{getMethods().map((item) => (
|
||||
<Pressable
|
||||
key={item.title}
|
||||
onPress={() => {
|
||||
setCurrentMethod({
|
||||
method: item.id,
|
||||
isPrimary: false
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
padding: Spacing.LEVEL_2,
|
||||
backgroundColor: colors.secondary.background,
|
||||
flexDirection: "row",
|
||||
borderRadius: Radius.S,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
borderRadius: Radius.XS,
|
||||
padding: Spacing.LEVEL_1,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
backgroundColor: RGB_Linear_Shade(
|
||||
0.04,
|
||||
hexToRGBA(colors.secondary.background)
|
||||
)
|
||||
}}
|
||||
size={17}
|
||||
color={colors.primary.icon}
|
||||
name={item.icon}
|
||||
iconFamily={item.iconFamily as "notesnook"}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
type="secondaryAccented"
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={15}
|
||||
color={colors.primary.accent}
|
||||
name={item.icon}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.md}>{item.title}</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<Paragraph size={AppFontSize.sm}>{item.title}</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,19 +21,16 @@ import { strings } from "@notesnook/intl";
|
||||
import { useRef, useState } from "react";
|
||||
import { TextInput } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import TwoFactorVerification from "./two-factor";
|
||||
import { createFormRef } from "../ui/input/form-input";
|
||||
|
||||
export const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
mfaAuth: 2,
|
||||
passwordAuth: 3
|
||||
mfaAuth: 2
|
||||
};
|
||||
|
||||
export const useLogin = (
|
||||
@@ -46,6 +43,7 @@ export const useLogin = (
|
||||
const [step, setStep] = useState(LoginSteps.emailAuth);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const mfaData = useRef<any>(undefined);
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: "",
|
||||
@@ -60,57 +58,23 @@ export const useLogin = (
|
||||
setLoading(true);
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
if (formRef.current.validateField("email")) {
|
||||
if (!formRef.current.validate()) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const mfaInfo = await db.user.authenticateEmail(
|
||||
formRef.current.getValue("email")
|
||||
);
|
||||
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(
|
||||
async (mfa: any, callback: (success: boolean) => void, onerror: (e: Error) => void) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
|
||||
if (success) {
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 500);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
ToastManager.error(new Error("Token expired, try logging in again"));
|
||||
} else {
|
||||
onerror(e as Error);
|
||||
}
|
||||
}
|
||||
},
|
||||
mfaInfo,
|
||||
() => {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
);
|
||||
mfaData.current = mfaInfo;
|
||||
setStep(LoginSteps.mfaAuth);
|
||||
setLoading(false);
|
||||
} else {
|
||||
finishWithError(new Error(strings.unableToSend2faCode()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
case LoginSteps.mfaAuth: {
|
||||
if (!formRef.current.validate()) {
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -172,6 +136,7 @@ export const useLogin = (
|
||||
setLoading,
|
||||
error,
|
||||
setError,
|
||||
formRef
|
||||
formRef,
|
||||
mfaData
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,8 +31,7 @@ import ImagePreview from "../image-preview";
|
||||
import MergeConflicts from "../merge-conflicts";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import Progress from "../dialogs/progress";
|
||||
import Progress from "../dialogs/progress/progress";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const DialogProvider = () => {
|
||||
@@ -51,7 +50,6 @@ const DialogProvider = () => {
|
||||
{isAppLoading ? null : (
|
||||
<>
|
||||
<MergeConflicts />
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
|
||||
@@ -20,14 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { View } from "react-native";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { getColorLinearShade } from "../../utils/colors";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Button, ButtonProps } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
const DialogButtons = ({
|
||||
onPressPositive,
|
||||
@@ -51,82 +48,41 @@ const DialogButtons = ({
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: colors.secondary.background,
|
||||
height: 60,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderTopWidth: 0.7,
|
||||
borderTopColor: getColorLinearShade(
|
||||
colors.secondary.background,
|
||||
0.05,
|
||||
isDark
|
||||
)
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}
|
||||
]}
|
||||
>
|
||||
{doneText ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
color={colors.primary.accent}
|
||||
name="check-circle-outline"
|
||||
size={AppFontSize.md}
|
||||
/>
|
||||
<Paragraph color={colors.primary.accent}>{" " + doneText}</Paragraph>
|
||||
</View>
|
||||
) : loading ? (
|
||||
<ActivityIndicator
|
||||
size={AppFontSize.lg}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
) : (
|
||||
<View />
|
||||
)}
|
||||
|
||||
<View
|
||||
<Button
|
||||
onPress={onPressNegative}
|
||||
fontSize={AppFontSize.md}
|
||||
testID={notesnook.ids.default.dialog.no}
|
||||
type="plain-outline"
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
flexGrow: 1,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
title={negativeTitle}
|
||||
/>
|
||||
{onPressPositive ? (
|
||||
<Button
|
||||
onPress={onPressNegative}
|
||||
fontSize={AppFontSize.sm}
|
||||
testID={notesnook.ids.default.dialog.no}
|
||||
type="plain"
|
||||
onPress={onPressPositive}
|
||||
fontSize={AppFontSize.md}
|
||||
testID={notesnook.ids.default.dialog.yes}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1
|
||||
}}
|
||||
loading={loading}
|
||||
bold
|
||||
title={negativeTitle}
|
||||
type={positiveType || "accent"}
|
||||
title={positiveTitle}
|
||||
/>
|
||||
{onPressPositive ? (
|
||||
<Button
|
||||
onPress={onPressPositive}
|
||||
fontSize={AppFontSize.sm}
|
||||
testID={notesnook.ids.default.dialog.yes}
|
||||
style={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
bold
|
||||
type={positiveType || "transparent"}
|
||||
title={positiveTitle}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogButtons;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import { DDS } from "../../services/device-detection";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { Radius } from "../../common/design/spacing";
|
||||
|
||||
const DialogContainer = ({
|
||||
width,
|
||||
@@ -44,9 +45,10 @@ const DialogContainer = ({
|
||||
{
|
||||
width: width || DDS.isTab ? 500 : "85%",
|
||||
maxHeight: height || 450,
|
||||
borderRadius: 10,
|
||||
borderRadius: Radius.LG,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
paddingTop: 12,
|
||||
marginTop: -300
|
||||
},
|
||||
restProps?.noBorder
|
||||
? {}
|
||||
|
||||
@@ -17,17 +17,20 @@ 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 { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Text, View, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import AppIcon, { IconProps } from "../ui/AppIcon";
|
||||
import { Button, ButtonProps } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
type DialogHeaderProps = {
|
||||
icon?: string;
|
||||
iconFamily?: IconProps["iconFamily"];
|
||||
iconType?: "error" | "normal";
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
button?: ButtonProps;
|
||||
@@ -46,7 +49,10 @@ const DialogHeader = ({
|
||||
padding,
|
||||
centered,
|
||||
titlePart,
|
||||
style
|
||||
style,
|
||||
icon,
|
||||
iconFamily,
|
||||
iconType
|
||||
}: DialogHeaderProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
@@ -57,61 +63,89 @@ const DialogHeader = ({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
minHeight: 50,
|
||||
paddingHorizontal: padding,
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
gap: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
{icon ? (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: centered ? "center" : "flex-start",
|
||||
width: 40,
|
||||
height: 40,
|
||||
backgroundColor: colors.error.shade,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderRadius: Radius.XS
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name={icon}
|
||||
iconFamily={iconFamily}
|
||||
color={
|
||||
iconType == "error" ? colors.static.red : colors.primary.icon
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: centered ? "center" : "space-between",
|
||||
alignItems: "center"
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{ textAlign: centered ? "center" : "left" }}
|
||||
size={AppFontSize.lg}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: centered ? "center" : "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{title}{" "}
|
||||
{titlePart ? (
|
||||
<Text style={{ color: colors.primary.accent }}>
|
||||
{titlePart}
|
||||
</Text>
|
||||
) : null}
|
||||
</Heading>
|
||||
<Heading
|
||||
style={{ textAlign: centered ? "center" : "left" }}
|
||||
fontSize="XL"
|
||||
>
|
||||
{title}{" "}
|
||||
{titlePart ? (
|
||||
<Text style={{ color: colors.primary.accent }}>
|
||||
{titlePart}
|
||||
</Text>
|
||||
) : null}
|
||||
</Heading>
|
||||
|
||||
{button ? (
|
||||
<Button
|
||||
{button ? (
|
||||
<Button
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
fontSize={13}
|
||||
type={button.type || "secondary"}
|
||||
height={30}
|
||||
{...button}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{paragraph ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
textAlign: centered ? "center" : "left",
|
||||
maxWidth: centered ? "90%" : "100%",
|
||||
alignSelf: centered ? "center" : "flex-start"
|
||||
}}
|
||||
fontSize={13}
|
||||
type={button.type || "secondary"}
|
||||
height={30}
|
||||
{...button}
|
||||
/>
|
||||
color={paragraphColor || colors.secondary.paragraph}
|
||||
>
|
||||
{paragraph}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{paragraph ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: centered ? "center" : "left",
|
||||
maxWidth: centered ? "90%" : "100%",
|
||||
alignSelf: centered ? "center" : "flex-start"
|
||||
}}
|
||||
color={paragraphColor || colors.secondary.paragraph}
|
||||
>
|
||||
{paragraph}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -23,10 +23,12 @@ import { eCloseSimpleDialog, eOpenSimpleDialog } from "../../utils/events";
|
||||
import { ButtonProps } from "../ui/button";
|
||||
import { FieldValidator, FormRef } from "../ui/input/form-input";
|
||||
import { RefObject } from "react";
|
||||
import { IconProps } from "../ui/AppIcon";
|
||||
|
||||
export type DialogInfo = {
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
centered?: boolean;
|
||||
positiveText: string;
|
||||
negativeText: string;
|
||||
background?: string;
|
||||
@@ -44,6 +46,8 @@ export type DialogInfo = {
|
||||
| "error"
|
||||
| "errorShade";
|
||||
icon?: string;
|
||||
iconFamily?: IconProps["iconFamily"];
|
||||
iconType?: "error" | "normal";
|
||||
paragraphColor: string;
|
||||
form?: {
|
||||
formRef: FormRef;
|
||||
@@ -58,6 +62,7 @@ export type DialogInfo = {
|
||||
onFormSubmit?: (form: FormRef) => Promise<boolean>;
|
||||
};
|
||||
input: boolean;
|
||||
inputLabel?: string;
|
||||
inputPlaceholder: string;
|
||||
defaultValue: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
|
||||
@@ -19,13 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
RefObject
|
||||
useState
|
||||
} from "react";
|
||||
import { TextInput, View, ViewStyle } from "react-native";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
@@ -42,7 +43,6 @@ import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { FormInput, type FormRef } from "../ui/input/form-input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Seperator from "../ui/seperator";
|
||||
import BaseDialog from "./base-dialog";
|
||||
import DialogButtons from "./dialog-buttons";
|
||||
import DialogHeader from "./dialog-header";
|
||||
@@ -143,9 +143,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
}, [hide, show]);
|
||||
|
||||
const onNegativePress = async () => {
|
||||
if (dialogInfo?.onClose) {
|
||||
await dialogInfo.onClose();
|
||||
}
|
||||
hide();
|
||||
};
|
||||
|
||||
@@ -155,7 +152,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
maxHeight: 450,
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12,
|
||||
gap: Spacing.LEVEL_4,
|
||||
...getContainerBorder(colors.primary.border, 0.5),
|
||||
overflow: "hidden"
|
||||
};
|
||||
@@ -170,9 +167,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
bounce={!dialogInfo.input && !dialogInfo.form}
|
||||
closeOnTouch={!dialogInfo.disableBackdropClosing}
|
||||
background={dialogInfo.background}
|
||||
transparent={
|
||||
dialogInfo.transparent === undefined ? false : dialogInfo.transparent
|
||||
}
|
||||
transparent={dialogInfo.transparent}
|
||||
onShow={async () => {
|
||||
if (dialogInfo.input && !dialogInfo.form) {
|
||||
inputRef.current?.setNativeProps({
|
||||
@@ -201,54 +196,65 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
: dialogInfo.component}
|
||||
|
||||
{dialogInfo.component ? null : (
|
||||
<View style={style}>
|
||||
<DialogHeader
|
||||
title={dialogInfo.title}
|
||||
icon={dialogInfo.icon}
|
||||
paragraph={dialogInfo.paragraph}
|
||||
paragraphColor={dialogInfo.paragraphColor}
|
||||
padding={12}
|
||||
style={{
|
||||
minHeight: 0
|
||||
}}
|
||||
/>
|
||||
<Seperator half />
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
paddingVertical: Spacing.LEVEL_4
|
||||
},
|
||||
style
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
gap: Spacing.LEVEL_4
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DialogHeader
|
||||
title={dialogInfo.title}
|
||||
icon={dialogInfo.icon}
|
||||
iconFamily={dialogInfo.iconFamily}
|
||||
iconType={dialogInfo.iconType}
|
||||
centered={dialogInfo.centered}
|
||||
paragraph={dialogInfo.paragraph}
|
||||
paragraphColor={dialogInfo.paragraphColor}
|
||||
style={{
|
||||
minHeight: 0
|
||||
}}
|
||||
/>
|
||||
|
||||
{dialogInfo.form ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP / 2
|
||||
}}
|
||||
>
|
||||
{dialogInfo.form.items.map((item, index) => (
|
||||
<FormInput
|
||||
key={item.name}
|
||||
fwdRef={item.ref}
|
||||
name={item.name}
|
||||
autoFocus={index === 0}
|
||||
placeholder={item.placeholder}
|
||||
formRef={formRef as RefObject<FormRef>}
|
||||
validators={item.validators}
|
||||
defaultValue={item.defaultValue}
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
onSubmitEditing={() => {
|
||||
const nextItem = dialogInfo?.form?.items?.[index + 1];
|
||||
if (nextItem) {
|
||||
nextItem?.ref.current?.focus();
|
||||
} else {
|
||||
onPressPositive();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : dialogInfo.input ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
{dialogInfo.form ? (
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{dialogInfo.form.items.map((item, index) => (
|
||||
<FormInput
|
||||
key={item.name}
|
||||
fwdRef={item.ref}
|
||||
name={item.name}
|
||||
label={item.label}
|
||||
autoFocus={index === 0}
|
||||
placeholder={item.placeholder}
|
||||
formRef={formRef as RefObject<FormRef>}
|
||||
validators={item.validators}
|
||||
defaultValue={item.defaultValue}
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
onSubmitEditing={() => {
|
||||
const nextItem = dialogInfo?.form?.items?.[index + 1];
|
||||
if (nextItem) {
|
||||
nextItem?.ref.current?.focus();
|
||||
} else {
|
||||
onPressPositive();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : dialogInfo.input ? (
|
||||
<Input
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
@@ -258,6 +264,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
testID="input-value"
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
defaultValue={dialogInfo.defaultValue}
|
||||
label={dialogInfo.inputLabel}
|
||||
onSubmit={() => {
|
||||
onPressPositive();
|
||||
}}
|
||||
@@ -266,48 +273,47 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
keyboardType={dialogInfo.keyboardType || "default"}
|
||||
placeholder={dialogInfo.inputPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
) : null}
|
||||
|
||||
{dialogInfo?.notice ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Notice
|
||||
type={dialogInfo.notice.type || "information"}
|
||||
text={dialogInfo.notice.text}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo.check ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setChecked(!checked);
|
||||
}}
|
||||
icon={
|
||||
checked
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
iconColor={
|
||||
checked ? colors.secondary.icon : colors.primary.icon
|
||||
}
|
||||
{dialogInfo?.notice ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-start"
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
height={35}
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={checked ? dialogInfo.check.type || "plain" : "plain"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
>
|
||||
<Notice
|
||||
type={dialogInfo.notice.type || "information"}
|
||||
text={dialogInfo.notice.text}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo.check ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setChecked(!checked);
|
||||
}}
|
||||
icon={
|
||||
checked
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
iconColor={
|
||||
checked ? colors.secondary.icon : colors.primary.icon
|
||||
}
|
||||
style={{
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
height={35}
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={checked ? dialogInfo.check.type || "plain" : "plain"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
<DialogButtons
|
||||
onPressNegative={onNegativePress}
|
||||
onPressPositive={
|
||||
|
||||
@@ -16,9 +16,11 @@ 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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
clearAppLockVerificationCipher,
|
||||
@@ -33,7 +35,7 @@ import {
|
||||
eSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { PASSWORD_PLACEHOLDER } from "../../../utils/constants";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import {
|
||||
eCloseAppLocKPasswordDailog,
|
||||
@@ -42,61 +44,122 @@ import {
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Toast } from "../../toast";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import FormInput, {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../ui/input/form-input";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
|
||||
export const AppLockPassword = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [mode, setMode] = useState<"create" | "change" | "remove">("create");
|
||||
const [keyboardType, setKeyboardType] = useState<"pin" | "password">(
|
||||
useSettingStore.getState().settings.applockKeyboardType === "default"
|
||||
? "password"
|
||||
: "pin"
|
||||
);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const currentPasswordInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const confirmPasswordInputRef = useRef<TextInput>(null);
|
||||
const values = useRef<{
|
||||
currentPassword?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
}>({});
|
||||
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
currentPassword: "",
|
||||
password: "",
|
||||
confirmPassword: ""
|
||||
})
|
||||
);
|
||||
const [accountPass, setAccountPass] = useState(false);
|
||||
const enableApplock = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const subs = [
|
||||
eSubscribeEvent(
|
||||
eOpenAppLockPasswordDialog,
|
||||
(mode: "create" | "change" | "remove") => {
|
||||
(mode: "create" | "change" | "remove", _enableApplock = false) => {
|
||||
setMode(mode);
|
||||
setAccountPass(false);
|
||||
setVisible(true);
|
||||
enableApplock.current = _enableApplock;
|
||||
}
|
||||
),
|
||||
eSubscribeEvent(eCloseAppLocKPasswordDailog, () => {
|
||||
values.current = {};
|
||||
setVisible(false);
|
||||
close();
|
||||
})
|
||||
];
|
||||
return () => {
|
||||
subs.forEach((sub) => sub?.unsubscribe());
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const close = () => {
|
||||
values.current = {};
|
||||
formRef.current.setValue("currentPassword", "");
|
||||
formRef.current.setValue("password", "");
|
||||
formRef.current.setValue("confirmPassword", "");
|
||||
formRef.current.clearErrors();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.current.validate()) return;
|
||||
|
||||
const currentPassword = formRef.current.getValue("currentPassword");
|
||||
const password = formRef.current.getValue("password");
|
||||
|
||||
if (mode === "create") {
|
||||
setAppLockVerificationCipher(password);
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
if (enableApplock.current) {
|
||||
SettingsService.setProperty("appLockEnabled", true);
|
||||
}
|
||||
} else if (mode === "change") {
|
||||
const isCurrentPasswordCorrect =
|
||||
await validateAppLockPassword(currentPassword);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
formRef.current.setError(
|
||||
"currentPassword",
|
||||
strings.incorrect("password")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await clearAppLockVerificationCipher();
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
await setAppLockVerificationCipher(password);
|
||||
} else if (mode === "remove") {
|
||||
const isCurrentPasswordCorrect = accountPass
|
||||
? await db.user.verifyPassword(password)
|
||||
: await validateAppLockPassword(password);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
formRef.current.setError(
|
||||
"password",
|
||||
accountPass
|
||||
? strings.passwordIncorrect()
|
||||
: strings.incorrect("password")
|
||||
);
|
||||
return;
|
||||
}
|
||||
clearAppLockVerificationCipher();
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", false);
|
||||
|
||||
if (
|
||||
!(await BiometricService.isBiometryAvailable()) ||
|
||||
SettingsService.getProperty("biometricsAuthEnabled") === false
|
||||
) {
|
||||
SettingsService.setProperty("appLockEnabled", false);
|
||||
SettingsService.setPrivacyScreen(
|
||||
SettingsService.getProperty("privacyScreen")
|
||||
);
|
||||
ToastManager.show({
|
||||
message: strings.applockDisabled(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
close();
|
||||
};
|
||||
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
onShow={async () => {
|
||||
@@ -115,286 +178,132 @@ export const AppLockPassword = () => {
|
||||
style={{
|
||||
...getElevationStyle(10),
|
||||
width: DDS.isTab ? 350 : "85%",
|
||||
borderRadius: 10,
|
||||
borderRadius: Radius.MD,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: Spacing.LEVEL_4,
|
||||
gap: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={strings.changeAppLockCredentials(mode, keyboardType)}
|
||||
icon="shield"
|
||||
padding={12}
|
||||
/>
|
||||
<Seperator half />
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.changeAppLockCredentials(mode, "password")}
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<View style={{ gap: Spacing.LEVEL_2 }}>
|
||||
{mode === "change" ? (
|
||||
<Input
|
||||
<FormInput
|
||||
name="currentPassword"
|
||||
formRef={formRef}
|
||||
label={strings.currentPassword()}
|
||||
fwdRef={currentPasswordInputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
values.current.currentPassword = value;
|
||||
}}
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
defaultValue={values.current.currentPassword}
|
||||
autoComplete="password"
|
||||
onSubmitEditing={() => passwordInputRef.current?.focus()}
|
||||
returnKeyLabel={strings.next()}
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
returnKeyType="next"
|
||||
secureTextEntry={secureTextEntry}
|
||||
placeholder={
|
||||
keyboardType === "pin"
|
||||
? strings.currentPin()
|
||||
: strings.currentPassword()
|
||||
}
|
||||
secureTextEntry
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
containerStyle={{ borderRadius: Radius.XS }}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Input
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
label={
|
||||
mode === "change"
|
||||
? strings.newPassword()
|
||||
: strings.enterPassword()
|
||||
}
|
||||
fwdRef={passwordInputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
values.current.password = value;
|
||||
}}
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
defaultValue={values.current.password}
|
||||
keyboardType={
|
||||
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
|
||||
}
|
||||
autoComplete="password"
|
||||
onSubmitEditing={() => {
|
||||
if (mode !== "remove") {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
} else {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
returnKeyLabel={
|
||||
mode !== "remove" ? strings.next() : strings.remove()
|
||||
}
|
||||
returnKeyType={mode !== "remove" ? "next" : "done"}
|
||||
secureTextEntry={secureTextEntry}
|
||||
buttonLeft={
|
||||
accountPass ? null : (
|
||||
<IconButton
|
||||
name={keyboardType === "password" ? "numeric" : "keyboard"}
|
||||
onPress={() => {
|
||||
setKeyboardType(
|
||||
keyboardType === "password" ? "pin" : "password"
|
||||
);
|
||||
setSecureTextEntry(false);
|
||||
setImmediate(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={AppFontSize.lg}
|
||||
/>
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
accountPass
|
||||
? strings.enterAccountPassword()
|
||||
: mode === "change"
|
||||
? keyboardType === "pin"
|
||||
? strings.newPin()
|
||||
: strings.newPassword()
|
||||
: `${
|
||||
keyboardType === "pin"
|
||||
? strings.pin()
|
||||
: strings.password()
|
||||
}`
|
||||
}
|
||||
secureTextEntry
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
containerStyle={{ borderRadius: Radius.XS }}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
/>
|
||||
|
||||
{mode !== "remove" ? (
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
values.current.confirmPassword = value;
|
||||
}}
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
defaultValue={values.current.confirmPassword}
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
customValidator={() => values.current.password || ""}
|
||||
validationType="confirmPassword"
|
||||
autoComplete="password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry={secureTextEntry}
|
||||
placeholder={
|
||||
keyboardType === "pin"
|
||||
? strings.confirmPin()
|
||||
<FormInput
|
||||
name="confirmPassword"
|
||||
formRef={formRef}
|
||||
label={
|
||||
mode === "change"
|
||||
? strings.confirmNewPassword()
|
||||
: strings.confirmPassword()
|
||||
}
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
autoCapitalize="none"
|
||||
autoComplete="password"
|
||||
onSubmitEditing={() => onSubmit()}
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
containerStyle={{ borderRadius: Radius.XS }}
|
||||
validators={[
|
||||
validators.required(strings.confirmPasswordRequired()),
|
||||
validators.matchField("password", strings.passwordNotMatched())
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{mode === "remove" ? (
|
||||
<>
|
||||
<Button
|
||||
icon={
|
||||
accountPass ? "checkbox-marked" : "checkbox-blank-outline"
|
||||
}
|
||||
onPress={() => {
|
||||
setSecureTextEntry(false);
|
||||
setAccountPass(!accountPass);
|
||||
setTimeout(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
iconSize={AppFontSize.lg}
|
||||
type="plain"
|
||||
iconColor={
|
||||
accountPass ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
title={strings.useAccountPassword()}
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<Button
|
||||
icon={accountPass ? "checkbox" : "box-empty"}
|
||||
iconFamily="notesnook"
|
||||
onPress={() => setAccountPass(!accountPass)}
|
||||
iconSize={AppFontSize.sm}
|
||||
fontFamily="MEDIUM"
|
||||
fontSize={AppFontSize.xs}
|
||||
type="plain"
|
||||
iconColor={
|
||||
accountPass
|
||||
? [colors.primary.accent, colors.primary.accentForeground]
|
||||
: colors.primary.icon
|
||||
}
|
||||
textStyle={{
|
||||
color: colors.primary.paragraph
|
||||
}}
|
||||
title={strings.useAccountPassword()}
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: 0
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<DialogButtons
|
||||
onPressNegative={close}
|
||||
onPressPositive={async () => {
|
||||
if (mode === "create") {
|
||||
if (!values.current.password || !values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error(strings.allFieldsRequired()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.current.password !== values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error(strings.mismatch(keyboardType)),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const password = values.current.password;
|
||||
setAppLockVerificationCipher(password);
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
} else if (mode === "change") {
|
||||
if (
|
||||
!values.current.currentPassword ||
|
||||
!values.current.password ||
|
||||
!values.current.confirmPassword
|
||||
) {
|
||||
ToastManager.error(
|
||||
new Error(strings.allFieldsRequired()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.current.password !== values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error(strings.mismatch(keyboardType)),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.currentPassword
|
||||
);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(
|
||||
new Error(strings.incorrect(keyboardType)),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const password = values.current.password;
|
||||
await clearAppLockVerificationCipher();
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
await setAppLockVerificationCipher(password);
|
||||
} else if (mode === "remove") {
|
||||
if (!values.current.password) {
|
||||
ToastManager.error(
|
||||
new Error(strings.allFieldsRequired()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = accountPass
|
||||
? await db.user.verifyPassword(values.current.password)
|
||||
: await validateAppLockPassword(values.current.password);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
accountPass
|
||||
? strings.passwordIncorrect()
|
||||
: strings.incorrect(keyboardType)
|
||||
),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
clearAppLockVerificationCipher();
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", false);
|
||||
|
||||
if (
|
||||
!(await BiometricService.isBiometryAvailable()) ||
|
||||
SettingsService.getProperty("biometricsAuthEnabled") === false
|
||||
) {
|
||||
SettingsService.setProperty("appLockEnabled", false);
|
||||
SettingsService.setPrivacyScreen(
|
||||
SettingsService.getProperty("privacyScreen")
|
||||
);
|
||||
ToastManager.show({
|
||||
message: strings.applockDisabled(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SettingsService.setProperty(
|
||||
"applockKeyboardType",
|
||||
keyboardType === "password" ? "default" : "numeric"
|
||||
);
|
||||
|
||||
close();
|
||||
}}
|
||||
positiveTitle={
|
||||
mode === "remove"
|
||||
? strings.remove()
|
||||
: mode === "change"
|
||||
? strings.change()
|
||||
: strings.save()
|
||||
}
|
||||
negativeTitle={strings.cancel()}
|
||||
positiveType="transparent"
|
||||
loading={false}
|
||||
doneText=""
|
||||
/>
|
||||
<View style={{ flexDirection: "row", gap: Spacing.LEVEL_2 }}>
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="plain-outline"
|
||||
onPress={close}
|
||||
style={{ flex: 1, paddingVertical: Spacing.LEVEL_3 }}
|
||||
/>
|
||||
<Button
|
||||
title={mode === "remove" ? strings.remove() : strings.save()}
|
||||
type="accent"
|
||||
onPress={onSubmit}
|
||||
style={{ flex: 1, paddingVertical: Spacing.LEVEL_3 }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Toast context="local" />
|
||||
@@ -402,6 +311,9 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
};
|
||||
|
||||
AppLockPassword.present = (mode: "create" | "change" | "remove") => {
|
||||
eSendEvent(eOpenAppLockPasswordDialog, mode);
|
||||
AppLockPassword.present = (
|
||||
mode: "create" | "change" | "remove",
|
||||
enableAppLock?: boolean
|
||||
) => {
|
||||
eSendEvent(eOpenAppLockPasswordDialog, mode, enableAppLock);
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ import { IconButton } from "../../ui/icon-button";
|
||||
import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import ReactNativeBlobUtil from "react-native-blob-util";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
|
||||
const WIN_WIDTH = Dimensions.get("window").width;
|
||||
const WIN_HEIGHT = Dimensions.get("window").height;
|
||||
@@ -286,6 +287,9 @@ const PDFPreview = () => {
|
||||
color={colors.static.white}
|
||||
name="open-in-new"
|
||||
onPress={() => {
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
FileViewer.open(pdfSource, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
|
||||
@@ -17,28 +17,8 @@ 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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { eSendEvent, eSubscribeEvent } from "../../../services/event-manager";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogContainer from "../../dialog/dialog-container";
|
||||
import { Button } from "../../ui/button";
|
||||
import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
export type ProgressOptions = {
|
||||
progress?: string;
|
||||
cancelCallback?: () => void;
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
fillBackground?: boolean;
|
||||
canHideProgress?: boolean;
|
||||
};
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import { ProgressOptions } from "./progress";
|
||||
|
||||
export const PROGRESS_EVENTS = {
|
||||
start: "startProgress",
|
||||
@@ -46,146 +26,6 @@ export const PROGRESS_EVENTS = {
|
||||
update: "updateProgress"
|
||||
};
|
||||
|
||||
export default function Progress() {
|
||||
const { colors } = useThemeColors();
|
||||
const [progress, setProgress] = React.useState<string | undefined>();
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const cancelCallback = React.useRef<() => void>(undefined);
|
||||
const [data, setData] = React.useState<{
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
fillBackground?: boolean;
|
||||
canHideProgress?: boolean;
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
|
||||
setData({
|
||||
title: options.title,
|
||||
paragraph: options.paragraph,
|
||||
fillBackground: options.fillBackground,
|
||||
canHideProgress: options.canHideProgress
|
||||
});
|
||||
setVisible(true);
|
||||
}),
|
||||
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
|
||||
setProgress(undefined);
|
||||
setVisible(false);
|
||||
setData(undefined);
|
||||
cancelCallback.current?.();
|
||||
cancelCallback.current = undefined;
|
||||
}),
|
||||
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
if (options.cancelCallback) {
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
}
|
||||
|
||||
const data: ProgressOptions = {};
|
||||
if (options.title) data.title = options.title;
|
||||
if (options.paragraph) data.paragraph = options.paragraph;
|
||||
if (options.fillBackground)
|
||||
data.fillBackground = options.fillBackground;
|
||||
|
||||
setData((current) => {
|
||||
return {
|
||||
...current,
|
||||
...data
|
||||
};
|
||||
});
|
||||
})
|
||||
];
|
||||
return () => {
|
||||
events.forEach((event) => event?.unsubscribe());
|
||||
cancelCallback.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
background={data?.fillBackground ? colors.primary.background : undefined}
|
||||
visible
|
||||
>
|
||||
<DialogContainer
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
noBorder={data?.fillBackground ? true : false}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: 10,
|
||||
paddingBottom: 20
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
size={AppFontSize.lg}
|
||||
>
|
||||
{data?.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{progress ? progress : data?.paragraph}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{!data?.canHideProgress ? null : (
|
||||
<Button
|
||||
title={cancelCallback.current ? "Cancel" : "Hide"}
|
||||
type="secondaryAccented"
|
||||
onPress={() => {
|
||||
if (cancelCallback.current) {
|
||||
cancelCallback.current?.();
|
||||
}
|
||||
setVisible(false);
|
||||
setProgress(undefined);
|
||||
setData(undefined);
|
||||
}}
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</DialogContainer>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function startProgress(options: ProgressOptions) {
|
||||
eSendEvent(PROGRESS_EVENTS.start, options);
|
||||
}
|
||||
|
||||
210
apps/mobile/app/components/dialogs/progress/progress.tsx
Normal file
210
apps/mobile/app/components/dialogs/progress/progress.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { PROGRESS_EVENTS } from ".";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { eSubscribeEvent } from "../../../services/event-manager";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogContainer from "../../dialog/dialog-container";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Button } from "../../ui/button";
|
||||
import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
export type ProgressOptions = {
|
||||
progress?: string;
|
||||
cancelCallback?: () => void;
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
fillBackground?: boolean;
|
||||
canHideProgress?: boolean;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
export default function Progress() {
|
||||
const { colors } = useThemeColors();
|
||||
const [progress, setProgress] = React.useState<string | undefined>();
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const cancelCallback = React.useRef<() => void>(undefined);
|
||||
const [data, setData] = React.useState<{
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
fillBackground?: boolean;
|
||||
canHideProgress?: boolean;
|
||||
icon?: string;
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
|
||||
setData({
|
||||
...options
|
||||
});
|
||||
setVisible(true);
|
||||
}),
|
||||
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
|
||||
setProgress(undefined);
|
||||
setVisible(false);
|
||||
setData(undefined);
|
||||
cancelCallback.current?.();
|
||||
cancelCallback.current = undefined;
|
||||
}),
|
||||
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
if (options.cancelCallback) {
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
}
|
||||
|
||||
const data: ProgressOptions = {};
|
||||
if (options.title) data.title = options.title;
|
||||
if (options.paragraph) data.paragraph = options.paragraph;
|
||||
if (options.fillBackground)
|
||||
data.fillBackground = options.fillBackground;
|
||||
if (options.icon) {
|
||||
data.icon = options.icon;
|
||||
}
|
||||
|
||||
setData((current) => {
|
||||
return {
|
||||
...current,
|
||||
...data
|
||||
};
|
||||
});
|
||||
})
|
||||
];
|
||||
return () => {
|
||||
events.forEach((event) => event?.unsubscribe());
|
||||
cancelCallback.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
background={data?.fillBackground ? colors.primary.background : undefined}
|
||||
visible
|
||||
>
|
||||
<DialogContainer
|
||||
style={{
|
||||
padding: Spacing.LEVEL_4
|
||||
}}
|
||||
noBorder={data?.fillBackground ? true : false}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_4,
|
||||
paddingTop: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{data?.icon ? (
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name={data?.icon}
|
||||
iconFamily="notesnook"
|
||||
size={20}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.heading}
|
||||
size={AppFontSize.lg}
|
||||
>
|
||||
{data?.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{progress ? progress : data?.paragraph}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={8}
|
||||
width={null}
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!data?.canHideProgress ? null : (
|
||||
<Button
|
||||
title={cancelCallback.current ? "Cancel" : "Hide"}
|
||||
type="accent"
|
||||
onPress={() => {
|
||||
if (cancelCallback.current) {
|
||||
cancelCallback.current?.();
|
||||
}
|
||||
setVisible(false);
|
||||
setProgress(undefined);
|
||||
setData(undefined);
|
||||
}}
|
||||
width={160}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</DialogContainer>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
@@ -55,7 +55,6 @@ import FormInput, {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../ui/input/form-input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
@@ -66,6 +65,10 @@ import {
|
||||
VAULT_ERRORS
|
||||
} from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { PASSWORD_PLACEHOLDER } from "../../../utils/constants";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
|
||||
export const VaultDialog: React.FC = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -86,7 +89,7 @@ export const VaultDialog: React.FC = () => {
|
||||
const paragraphRef = useRef<string | null>(null);
|
||||
const buttonTitleRef = useRef<string | null>(null);
|
||||
const positiveButtonTypeRef = useRef<"errorShade" | "transparent" | "accent">(
|
||||
"transparent"
|
||||
"accent"
|
||||
);
|
||||
const customActionTitleRef = useRef<string | null>(null);
|
||||
const customActionParagraphRef = useRef<string | null>(null);
|
||||
@@ -114,6 +117,7 @@ export const VaultDialog: React.FC = () => {
|
||||
const passInputRef = useRef<TextInput>(null);
|
||||
const confirmPassRef = useRef<TextInput>(null);
|
||||
const newPassInputRef = useRef<TextInput>(null);
|
||||
const [icon, setIcon] = useState<string>();
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (loading) {
|
||||
@@ -141,7 +145,7 @@ export const VaultDialog: React.FC = () => {
|
||||
descriptionRef.current = null;
|
||||
paragraphRef.current = null;
|
||||
buttonTitleRef.current = null;
|
||||
positiveButtonTypeRef.current = "transparent";
|
||||
positiveButtonTypeRef.current = "accent";
|
||||
customActionTitleRef.current = null;
|
||||
customActionParagraphRef.current = null;
|
||||
noteLockedRef.current = false;
|
||||
@@ -347,6 +351,7 @@ export const VaultDialog: React.FC = () => {
|
||||
async (note: Note & { content?: NoteContent<false> }) => {
|
||||
close();
|
||||
try {
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
await Share.open({
|
||||
title: note.title,
|
||||
failOnCancel: false,
|
||||
@@ -623,6 +628,7 @@ export const VaultDialog: React.FC = () => {
|
||||
setIsBiometryAvailable(available);
|
||||
setIsBiometryEnrolled(fingerprint);
|
||||
setBiometricUnlock(fingerprint);
|
||||
setIcon(data.icon);
|
||||
setDeleteAll(false);
|
||||
setLoading(false);
|
||||
|
||||
@@ -686,9 +692,9 @@ export const VaultDialog: React.FC = () => {
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
width: DDS.isTab ? 350 : "85%",
|
||||
borderRadius: 10,
|
||||
borderRadius: Radius.MD,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12,
|
||||
paddingVertical: Spacing.LEVEL_4,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
@@ -697,14 +703,21 @@ export const VaultDialog: React.FC = () => {
|
||||
paragraph={
|
||||
paragraphRef.current || customActionParagraphRef.current || ""
|
||||
}
|
||||
icon="shield"
|
||||
padding={12}
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
centered={!!icon}
|
||||
icon={icon}
|
||||
iconType="error"
|
||||
iconFamily="notesnook"
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
gap: isDeleteVault ? 0 : Spacing.LEVEL_4,
|
||||
marginTop: Spacing.LEVEL_4,
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{(isChangePassword ||
|
||||
@@ -717,20 +730,16 @@ export const VaultDialog: React.FC = () => {
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
label={
|
||||
isChangePassword
|
||||
? strings.currentPassword()
|
||||
: strings.password()
|
||||
}
|
||||
fwdRef={passInputRef}
|
||||
editable={!loading}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwd}
|
||||
autoComplete="password"
|
||||
marginBottom={
|
||||
!biometricUnlock ||
|
||||
!isBiometryEnrolled ||
|
||||
isCreateVault ||
|
||||
isChangePassword ||
|
||||
isCustomAction
|
||||
? 0
|
||||
: 10
|
||||
}
|
||||
onSubmitEditing={() => {
|
||||
if (isChangePassword) {
|
||||
newPassInputRef.current?.focus();
|
||||
@@ -743,11 +752,7 @@ export const VaultDialog: React.FC = () => {
|
||||
}
|
||||
returnKeyType={isChangePassword ? "next" : "done"}
|
||||
secureTextEntry
|
||||
placeholder={
|
||||
isChangePassword
|
||||
? strings.currentPassword()
|
||||
: strings.password()
|
||||
}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
/>
|
||||
|
||||
@@ -774,25 +779,30 @@ export const VaultDialog: React.FC = () => {
|
||||
{isDeleteVault && (
|
||||
<Button
|
||||
onPress={() => setDeleteAll(!deleteAll)}
|
||||
icon={
|
||||
deleteAll
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
icon={deleteAll ? "checkbox" : "box-empty"}
|
||||
iconFamily="notesnook"
|
||||
fontFamily="MEDIUM"
|
||||
iconSize={14}
|
||||
fontSize={AppFontSize.sm}
|
||||
width="100%"
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
title={strings.deleteAllNotes()}
|
||||
type="errorShade"
|
||||
type="transparent"
|
||||
iconColor={colors.error.accent}
|
||||
textStyle={{
|
||||
color: colors.error.accent
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isChangePassword ? (
|
||||
<>
|
||||
<Seperator half />
|
||||
<FormInput
|
||||
name="newPassword"
|
||||
label={strings.newPassword()}
|
||||
formRef={formRef}
|
||||
fwdRef={newPassInputRef}
|
||||
editable={!loading}
|
||||
@@ -805,7 +815,7 @@ export const VaultDialog: React.FC = () => {
|
||||
returnKeyLabel="Change"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
placeholder={strings.newPassword()}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
/>
|
||||
</>
|
||||
@@ -826,12 +836,13 @@ export const VaultDialog: React.FC = () => {
|
||||
onSubmitEditing={() => {
|
||||
confirmPassRef.current?.focus();
|
||||
}}
|
||||
placeholder={strings.password()}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
name="confirmPassword"
|
||||
label={strings.confirmPassword()}
|
||||
formRef={formRef}
|
||||
fwdRef={confirmPassRef}
|
||||
autoCapitalize="none"
|
||||
@@ -844,7 +855,7 @@ export const VaultDialog: React.FC = () => {
|
||||
onSubmitEditing={() => {
|
||||
onPress();
|
||||
}}
|
||||
placeholder={strings.confirmPassword()}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
validators={[
|
||||
validators.required(strings.confirmPasswordRequired()),
|
||||
validators.matchField(
|
||||
|
||||
@@ -17,25 +17,24 @@ 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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { View, ViewStyle } from "react-native";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { RouteName } from "../../stores/use-navigation-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { fluidTabsRef } from "../../utils/global-refs";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { IconButtonProps } from "../ui/icon-button";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { IconButton, IconButtonProps } from "../ui/icon-button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LeftMenus } from "./left-menus";
|
||||
import { RightMenus } from "./right-menus";
|
||||
|
||||
export const Header = ({
|
||||
renderedInRoute,
|
||||
@@ -45,7 +44,8 @@ export const Header = ({
|
||||
canGoBack,
|
||||
hasSearch,
|
||||
onSearch,
|
||||
rightButton
|
||||
rightButton,
|
||||
style
|
||||
}: {
|
||||
onLeftMenuButtonPress?: () => void;
|
||||
renderedInRoute?: RouteName;
|
||||
@@ -56,6 +56,7 @@ export const Header = ({
|
||||
hasSearch?: boolean;
|
||||
onSearch?: () => void;
|
||||
rightButton?: IconButtonProps;
|
||||
style?: ViewStyle;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [borderHidden, setBorderHidden] = useState(true);
|
||||
@@ -64,6 +65,9 @@ export const Header = ({
|
||||
state.selectionMode
|
||||
]);
|
||||
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const isTablet = deviceMode === "tablet";
|
||||
|
||||
const onScroll = useCallback(
|
||||
(data: { x: number; y: number; id?: string; route: string }) => {
|
||||
if (data.route !== renderedInRoute || data.id !== id) return;
|
||||
@@ -85,34 +89,61 @@ export const Header = ({
|
||||
};
|
||||
}, [borderHidden, onScroll]);
|
||||
|
||||
const HeaderWrapper = hasSearch ? Pressable : View;
|
||||
const _onLeftButtonPress = () => {
|
||||
if (onLeftMenuButtonPress) return onLeftMenuButtonPress();
|
||||
|
||||
if (!canGoBack) {
|
||||
if (fluidTabsRef.current?.isDrawerOpen()) {
|
||||
Navigation.closeDrawer();
|
||||
} else {
|
||||
Navigation.openDrawer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
Navigation.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginBottom: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<HeaderWrapper
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
borderRadius: 10,
|
||||
paddingVertical: 3,
|
||||
borderWidth: hasSearch ? 1 : 0,
|
||||
borderColor: colors.primary.border,
|
||||
paddingHorizontal: !hasSearch ? 0 : DefaultAppStyles.GAP_SMALL,
|
||||
alignItems: "center"
|
||||
}}
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
borderRadius: Radius.S,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_3,
|
||||
backgroundColor: colors.secondary.background,
|
||||
alignItems: "center"
|
||||
},
|
||||
style
|
||||
]}
|
||||
testID="search-header"
|
||||
onPress={() => {
|
||||
onSearch?.();
|
||||
}}
|
||||
>
|
||||
<LeftMenus
|
||||
canGoBack={canGoBack}
|
||||
onLeftButtonPress={onLeftMenuButtonPress}
|
||||
/>
|
||||
{isTablet && !canGoBack ? null : (
|
||||
<IconButton
|
||||
testID={notesnook.ids.default.header.buttons.left}
|
||||
left={40}
|
||||
top={40}
|
||||
onPress={_onLeftButtonPress}
|
||||
onLongPress={() => {
|
||||
Navigation.popToTop();
|
||||
}}
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20
|
||||
}}
|
||||
size={20}
|
||||
name={canGoBack ? "arrow-back" : "menu"}
|
||||
iconFamily="notesnook"
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!title ? (
|
||||
<View
|
||||
@@ -123,18 +154,39 @@ export const Header = ({
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
) : hasSearch ? (
|
||||
<Paragraph>
|
||||
{selectionMode
|
||||
? `${selectedItemsList.length} selected`
|
||||
: strings.searchInRoute(title)}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Heading size={AppFontSize.lg}>{title}</Heading>
|
||||
<Heading fontSize="XL">
|
||||
{selectionMode ? `${selectedItemsList.length} selected` : title}
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
<RightMenus rightButton={rightButton} />
|
||||
</HeaderWrapper>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{rightButton ? (
|
||||
<IconButton {...rightButton} color={colors.primary.icon} />
|
||||
) : null}
|
||||
|
||||
{hasSearch ? (
|
||||
<IconButton
|
||||
color={colors.primary.icon}
|
||||
size={20}
|
||||
onPress={() => {
|
||||
onSearch?.();
|
||||
}}
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20
|
||||
}}
|
||||
iconFamily="notesnook"
|
||||
name="search"
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { fluidTabsRef } from "../../utils/global-refs";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
export const LeftMenus = ({
|
||||
canGoBack,
|
||||
onLeftButtonPress
|
||||
}: {
|
||||
canGoBack?: boolean;
|
||||
onLeftButtonPress?: () => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const isTablet = deviceMode === "tablet";
|
||||
|
||||
const _onLeftButtonPress = () => {
|
||||
if (onLeftButtonPress) return onLeftButtonPress();
|
||||
|
||||
if (!canGoBack) {
|
||||
if (fluidTabsRef.current?.isDrawerOpen()) {
|
||||
Navigation.closeDrawer();
|
||||
} else {
|
||||
Navigation.openDrawer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
Navigation.goBack();
|
||||
};
|
||||
|
||||
return isTablet && !canGoBack ? null : (
|
||||
<IconButton
|
||||
testID={notesnook.ids.default.header.buttons.left}
|
||||
left={40}
|
||||
top={40}
|
||||
onPress={_onLeftButtonPress}
|
||||
onLongPress={() => {
|
||||
Navigation.popToTop();
|
||||
}}
|
||||
name={canGoBack ? "arrow-left" : "menu"}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
314
apps/mobile/app/components/intro/illustration.tsx
Normal file
314
apps/mobile/app/components/intro/illustration.tsx
Normal file
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
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 React from "react";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withSpring,
|
||||
withTiming
|
||||
} from "react-native-reanimated";
|
||||
import Svg, {
|
||||
Circle,
|
||||
ClipPath,
|
||||
Defs,
|
||||
G,
|
||||
Mask,
|
||||
Path,
|
||||
Rect
|
||||
} from "react-native-svg";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
const SPRING_CONFIG = {
|
||||
damping: 14,
|
||||
stiffness: 120,
|
||||
mass: 0.8
|
||||
};
|
||||
|
||||
// Each icon floats independently with a different phase/speed
|
||||
function useFloatAnim(delay: number, range = 6) {
|
||||
const y = useSharedValue(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const start = () => {
|
||||
y.value = withSequence(
|
||||
withTiming(0, { duration: delay, easing: Easing.linear }),
|
||||
withRepeat(
|
||||
withSequence(
|
||||
withTiming(-range, {
|
||||
duration: 1600,
|
||||
easing: Easing.inOut(Easing.sin)
|
||||
}),
|
||||
withTiming(range, {
|
||||
duration: 1600,
|
||||
easing: Easing.inOut(Easing.sin)
|
||||
})
|
||||
),
|
||||
-1, // infinite
|
||||
true // reverse
|
||||
)
|
||||
);
|
||||
};
|
||||
start();
|
||||
}, []);
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
export const IntroIllustration = () => {
|
||||
const { isDark } = useThemeColors();
|
||||
// Gesture-driven tilt
|
||||
const tiltX = useSharedValue(0); // rotateX: finger up/down
|
||||
const tiltY = useSharedValue(0); // rotateY: finger left/right
|
||||
const translateX = useSharedValue(0);
|
||||
const translateY = useSharedValue(0);
|
||||
|
||||
// Independent float offsets for each corner icon
|
||||
const floatTR = useFloatAnim(0, 2); // top-right lock
|
||||
const floatBR = useFloatAnim(500, 4); // bottom-right eye-off
|
||||
const floatTL = useFloatAnim(260, 3); // top-left share
|
||||
const floatBL = useFloatAnim(780, 2); // bottom-left document
|
||||
const pan = Gesture.Pan()
|
||||
.onUpdate((e) => {
|
||||
// Clamp tilt to ±15 degrees and translate ±12 pts
|
||||
tiltY.value = (e.translationX / 12) * 1;
|
||||
tiltX.value = -(e.translationY / 12) * 1;
|
||||
translateX.value = e.translationX * 0.08;
|
||||
translateY.value = e.translationY * 0.08;
|
||||
})
|
||||
.onEnd(() => {
|
||||
tiltX.value = withSpring(0, SPRING_CONFIG);
|
||||
tiltY.value = withSpring(0, SPRING_CONFIG);
|
||||
translateX.value = withSpring(0, SPRING_CONFIG);
|
||||
translateY.value = withSpring(0, SPRING_CONFIG);
|
||||
});
|
||||
|
||||
const containerStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{ perspective: 600 },
|
||||
{ rotateX: `${tiltX.value}deg` },
|
||||
{ rotateY: `${tiltY.value}deg` },
|
||||
{ translateX: translateX.value },
|
||||
{ translateY: translateY.value }
|
||||
]
|
||||
}));
|
||||
|
||||
// Derived animated styles for each floating icon
|
||||
const styleTR = useAnimatedStyle(() => ({
|
||||
transform: [{ translateY: floatTR.value }]
|
||||
}));
|
||||
const styleBR = useAnimatedStyle(() => ({
|
||||
transform: [{ translateY: floatBR.value }]
|
||||
}));
|
||||
const styleTL = useAnimatedStyle(() => ({
|
||||
transform: [{ translateY: floatTL.value }]
|
||||
}));
|
||||
const styleBL = useAnimatedStyle(() => ({
|
||||
transform: [{ translateY: floatBL.value }]
|
||||
}));
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={pan}>
|
||||
<Animated.View style={containerStyle}>
|
||||
<Svg width={243} height={210} viewBox="0 0 243 210" fill="none">
|
||||
{/* ── Concentric background circles (static) ── */}
|
||||
<Circle
|
||||
cx={116}
|
||||
cy={105}
|
||||
r={104.75}
|
||||
stroke={isDark ? "#1F2722" : "#E8F4ED"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Circle
|
||||
cx={116.5}
|
||||
cy={105.5}
|
||||
r={95.25}
|
||||
fill="#008836"
|
||||
fillOpacity={0.05}
|
||||
stroke={isDark ? "#233C2D" : "#E8F0EC"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Circle
|
||||
cx={116.5}
|
||||
cy={105.5}
|
||||
r={82.25}
|
||||
fill="#008836"
|
||||
fillOpacity={0.04}
|
||||
stroke={isDark ? "#233C2D" : "#E3F1E8"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Circle
|
||||
cx={116.5}
|
||||
cy={105.5}
|
||||
r={62.25}
|
||||
fill="#008836"
|
||||
fillOpacity={0.06}
|
||||
stroke={isDark ? "#233C2D" : "#D3E8DB"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Circle cx={116.5} cy={105.5} r={44.5} fill="#008836" />
|
||||
|
||||
{/* ── Defs ── */}
|
||||
<Defs>
|
||||
<ClipPath id="clip0">
|
||||
<Rect width={45} height={45} x={94.5} y={83} />
|
||||
</ClipPath>
|
||||
<ClipPath id="clip1">
|
||||
<Rect width={21.5} height={21.5} x={202.75} y={176.75} />
|
||||
</ClipPath>
|
||||
<Mask
|
||||
id="mask0"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x={91}
|
||||
y={80}
|
||||
width={52}
|
||||
height={51}
|
||||
>
|
||||
<Path
|
||||
d="M132.504 80.055H101.496C96.007 80.055 91.5574 84.5046 91.5574 89.9935V121.002C91.5574 126.491 96.007 130.94 101.496 130.94H132.504C137.993 130.94 142.443 126.491 142.443 121.002V89.9935C142.443 84.5046 137.993 80.055 132.504 80.055Z"
|
||||
fill="white"
|
||||
/>
|
||||
</Mask>
|
||||
</Defs>
|
||||
|
||||
{/* ── Centre logo (static) ── */}
|
||||
<G clipPath="url(#clip0)">
|
||||
<G mask="url(#mask0)">
|
||||
<Path
|
||||
d="M127.584 113.991C126.726 116.496 125.008 118.616 122.734 119.973C120.461 121.33 117.78 121.836 115.168 121.401C112.556 120.967 110.183 119.62 108.471 117.6C106.759 115.58 105.82 113.018 105.819 110.371V104.892L109.751 106.535V110.368C109.751 111.401 109.971 112.422 110.398 113.362C110.825 114.302 111.448 115.14 112.226 115.819C112.373 115.948 112.527 116.073 112.685 116.19C113.845 117.054 115.239 117.549 116.685 117.611C116.734 117.611 116.781 117.615 116.83 117.616C116.879 117.617 116.942 117.616 116.998 117.616H117.166C117.221 117.616 117.262 117.616 117.311 117.611C118.756 117.549 120.149 117.054 121.31 116.191C121.467 116.074 121.621 115.949 121.769 115.821C122.79 114.927 123.541 113.765 123.934 112.467L127.584 113.991ZM128.181 100.628V110.371C128.181 110.497 128.181 110.623 128.173 110.749L124.249 109.108V100.628C124.249 98.7551 123.523 96.9554 122.225 95.6055C120.928 94.2557 119.158 93.4604 117.287 93.3862C115.415 93.3121 113.588 93.9649 112.188 95.2078C110.787 96.4508 109.922 98.1875 109.773 100.054C109.759 100.243 109.751 100.435 109.751 100.628V103.048L105.819 101.405V89.4469H117C119.965 89.4469 122.809 90.6249 124.906 92.7217C127.003 94.8185 128.181 97.6624 128.181 100.628Z"
|
||||
fill="white"
|
||||
/>
|
||||
</G>
|
||||
</G>
|
||||
</Svg>
|
||||
|
||||
{/* ── Floating corner icons rendered as absolute Animated.Views over the SVG ── */}
|
||||
|
||||
{/* Top-left — share */}
|
||||
<Animated.View
|
||||
style={[{ position: "absolute", top: 0, left: -10 }, styleTL]}
|
||||
>
|
||||
<Svg width={47} height={47} viewBox="0 0 47 47" fill="none">
|
||||
<Rect
|
||||
x={0.25}
|
||||
y={0.25}
|
||||
width={46.5}
|
||||
height={46.5}
|
||||
rx={7.75}
|
||||
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
|
||||
stroke={isDark ? "#233C2D" : "#D3E8DB"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Rect
|
||||
width={22}
|
||||
height={22}
|
||||
x={13}
|
||||
y={12}
|
||||
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
|
||||
/>
|
||||
<Path
|
||||
d="M28.125 25.75C27.6663 25.7499 27.2124 25.8417 26.7899 26.0202C26.3674 26.1987 25.9851 26.4601 25.6654 26.789L21.7037 24.2427C22.0153 23.4435 22.0153 22.5565 21.7037 21.7573L25.6654 19.211C26.2605 19.8206 27.0607 20.1875 27.911 20.2406C28.7612 20.2937 29.6008 20.0292 30.2671 19.4984C30.9334 18.9675 31.3788 18.2082 31.5171 17.3677C31.6554 16.5271 31.4766 15.6651 31.0154 14.9488C30.5542 14.2326 29.8435 13.7131 29.0211 13.4911C28.1987 13.269 27.3231 13.3603 26.5641 13.7472C25.8051 14.134 25.2169 14.7889 24.9133 15.5849C24.6098 16.3808 24.6126 17.2612 24.9212 18.0552L20.9595 20.6015C20.4824 20.1117 19.8701 19.7753 19.2009 19.6353C18.5317 19.4953 17.8359 19.5581 17.2025 19.8156C16.5691 20.0731 16.0269 20.5136 15.6452 21.0808C15.2635 21.6481 15.0596 22.3163 15.0596 23C15.0596 23.6837 15.2635 24.3519 15.6452 24.9192C16.0269 25.4864 16.5691 25.9269 17.2025 26.1844C17.8359 26.4419 18.5317 26.5047 19.2009 26.3647C19.8701 26.2247 20.4824 25.8883 20.9595 25.3985L24.9212 27.9448C24.6558 28.6294 24.6165 29.3809 24.8091 30.0893C25.0018 30.7978 25.4161 31.4259 25.9916 31.8819C26.567 32.3378 27.2733 32.5975 28.007 32.6229C28.7408 32.6484 29.4634 32.4383 30.069 32.0233C30.6747 31.6084 31.1316 31.0105 31.3729 30.3171C31.6141 29.6237 31.627 28.8712 31.4097 28.17C31.1924 27.4687 30.7562 26.8554 30.1652 26.42C29.5741 25.9845 28.8591 25.7497 28.125 25.75ZM28.125 14.75C28.5329 14.75 28.9317 14.871 29.2708 15.0976C29.61 15.3242 29.8744 15.6463 30.0305 16.0232C30.1866 16.4001 30.2274 16.8148 30.1478 17.2149C30.0683 17.615 29.8718 17.9825 29.5834 18.2709C29.2949 18.5594 28.9274 18.7558 28.5273 18.8354C28.1273 18.915 27.7126 18.8741 27.3357 18.718C26.9588 18.5619 26.6367 18.2975 26.4101 17.9584C26.1834 17.6192 26.0625 17.2204 26.0625 16.8125C26.0625 16.2655 26.2798 15.7409 26.6666 15.3541C27.0533 14.9673 27.578 14.75 28.125 14.75ZM18.5 25.0625C18.092 25.0625 17.6933 24.9415 17.3541 24.7149C17.0149 24.4883 16.7506 24.1662 16.5945 23.7893C16.4384 23.4124 16.3975 22.9977 16.4771 22.5976C16.5567 22.1975 16.7531 21.83 17.0416 21.5416C17.33 21.2532 17.6975 21.0567 18.0976 20.9771C18.4977 20.8976 18.9124 20.9384 19.2892 21.0945C19.6661 21.2506 19.9882 21.515 20.2149 21.8541C20.4415 22.1933 20.5625 22.5921 20.5625 23C20.5625 23.547 20.3452 24.0716 19.9584 24.4584C19.5716 24.8452 19.047 25.0625 18.5 25.0625ZM28.125 31.25C27.717 31.25 27.3183 31.129 26.9791 30.9024C26.6399 30.6758 26.3756 30.3537 26.2195 29.9768C26.0634 29.5999 26.0225 29.1852 26.1021 28.7851C26.1817 28.385 26.3781 28.0175 26.6666 27.7291C26.955 27.4407 27.3225 27.2442 27.7226 27.1646C28.1227 27.0851 28.5374 27.1259 28.9142 27.282C29.2911 27.4381 29.6132 27.7025 29.8399 28.0416C30.0665 28.3808 30.1875 28.7796 30.1875 29.1875C30.1875 29.7345 29.9702 30.2591 29.5834 30.6459C29.1966 31.0327 28.672 31.25 28.125 31.25Z"
|
||||
fill="#008836"
|
||||
/>
|
||||
</Svg>
|
||||
</Animated.View>
|
||||
|
||||
{/* Top-right — lock */}
|
||||
<Animated.View
|
||||
style={[{ position: "absolute", top: 28, right: -5 }, styleTR]}
|
||||
>
|
||||
<Svg width={32} height={32} viewBox="0 0 32 32" fill="none">
|
||||
<Rect
|
||||
x={0.25}
|
||||
y={0.25}
|
||||
width={31.5}
|
||||
height={31.5}
|
||||
rx={7.75}
|
||||
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
|
||||
stroke={isDark ? "#233C2D" : "#D3E8DB"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Path
|
||||
d="M16 14.125C15.475 14.1252 14.967 14.3144 14.57 14.658C14.172 15.0017 13.912 15.4769 13.836 15.9967C13.76 16.5165 13.874 17.0462 14.157 17.4891C14.439 17.9319 14.872 18.2583 15.375 18.4086V19.75C15.375 19.9158 15.441 20.0747 15.558 20.1919C15.675 20.3092 15.834 20.375 16 20.375C16.166 20.375 16.325 20.3092 16.442 20.1919C16.559 20.0747 16.625 19.9158 16.625 19.75V18.4086C17.128 18.2583 17.561 17.9319 17.843 17.4891C18.126 17.0462 18.24 16.5165 18.164 15.9967C18.088 15.4769 17.828 15.0017 17.43 14.658C17.033 14.3144 16.525 14.1252 16 14.125ZM16 17.25C15.815 17.25 15.633 17.195 15.479 17.092C15.325 16.989 15.205 16.8426 15.134 16.6713C15.063 16.5 15.044 16.3115 15.081 16.1296C15.117 15.9477 15.206 15.7807 15.337 15.6496C15.468 15.5185 15.635 15.4292 15.817 15.393C15.999 15.3568 16.187 15.3754 16.359 15.4464C16.53 15.5173 16.676 15.6375 16.78 15.7917C16.883 15.9458 16.937 16.1271 16.937 16.3125C16.937 16.5611 16.839 16.7996 16.663 16.9754C16.487 17.1512 16.249 17.25 16 17.25ZM22.25 11.625H19.75V9.75C19.75 8.7554 19.355 7.8016 18.652 7.0983C17.948 6.3951 16.995 6 16 6C15.005 6 14.052 6.3951 13.348 7.0983C12.645 7.8016 12.25 8.7554 12.25 9.75V11.625H9.75C9.418 11.625 9.101 11.7567 8.866 11.9911C8.632 12.2255 8.5 12.5435 8.5 12.875V21.625C8.5 21.9565 8.632 22.2745 8.866 22.5089C9.101 22.7433 9.418 22.875 9.75 22.875H22.25C22.582 22.875 22.899 22.7433 23.134 22.5089C23.368 22.2745 23.5 21.9565 23.5 21.625V12.875C23.5 12.5435 23.368 12.2255 23.134 11.9911C22.899 11.7567 22.582 11.625 22.25 11.625ZM13.5 9.75C13.5 9.087 13.763 8.4511 14.232 7.9822C14.701 7.5134 15.337 7.25 16 7.25C16.663 7.25 17.299 7.5134 17.768 7.9822C18.237 8.4511 18.5 9.087 18.5 9.75V11.625H13.5V9.75ZM22.25 21.625H9.75V12.875H22.25V21.625Z"
|
||||
fill="#008836"
|
||||
/>
|
||||
</Svg>
|
||||
</Animated.View>
|
||||
|
||||
{/* Bottom-left — document */}
|
||||
<Animated.View
|
||||
style={[{ position: "absolute", bottom: 0, left: 25 }, styleBL]}
|
||||
>
|
||||
<Svg width={35} height={35} viewBox="0 0 35 35" fill="none">
|
||||
<Rect
|
||||
x={0.25}
|
||||
y={0.25}
|
||||
width={34.5}
|
||||
height={34.5}
|
||||
rx={7.75}
|
||||
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
|
||||
stroke={isDark ? "#233C2D" : "#D3E8DB"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Rect
|
||||
width={18.3333}
|
||||
height={18.3333}
|
||||
x={8.3334}
|
||||
y={8.333}
|
||||
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
|
||||
/>
|
||||
<Path
|
||||
d="M23.6346 14.178L19.6242 10.168C19.5709 10.114 19.5077 10.072 19.4382 10.044C19.3686 10.015 19.2941 10 19.2188 10H12.3438C12.0399 10 11.7485 10.121 11.5336 10.336C11.3187 10.551 11.198 10.842 11.198 11.146V23.75C11.198 24.054 11.3187 24.345 11.5336 24.56C11.7485 24.775 12.0399 24.896 12.3438 24.896H22.6563C22.9602 24.896 23.2517 24.775 23.4666 24.56C23.6814 24.345 23.8022 24.054 23.8022 23.75V14.583C23.8022 14.508 23.7875 14.434 23.7587 14.364C23.7299 14.295 23.6878 14.231 23.6346 14.178ZM19.7917 11.956L21.8464 14.01H19.7917V11.956ZM22.6563 23.75H12.3438V11.146H18.6459V14.583C18.6459 14.735 18.7063 14.881 18.8137 14.989C18.9212 15.096 19.0669 15.156 19.2188 15.156H22.6563V23.75ZM20.3647 18.021C20.3647 18.173 20.3043 18.319 20.1969 18.426C20.0894 18.533 19.9437 18.594 19.7917 18.594H15.2084C15.0565 18.594 14.9107 18.533 14.8033 18.426C14.6959 18.319 14.6355 18.173 14.6355 18.021C14.6355 17.869 14.6959 17.723 14.8033 17.616C14.9107 17.508 15.0565 17.448 15.2084 17.448H19.7917C19.9437 17.448 20.0894 17.508 20.1969 17.616C20.3043 17.723 20.3647 17.869 20.3647 18.021ZM20.3647 20.313C20.3647 20.465 20.3043 20.61 20.1969 20.718C20.0894 20.825 19.9437 20.885 19.7917 20.885H15.2084C15.0565 20.885 14.9107 20.825 14.8033 20.718C14.6959 20.61 14.6355 20.465 14.6355 20.313C14.6355 20.161 14.6959 20.015 14.8033 19.907C14.9107 19.8 15.0565 19.74 15.2084 19.74H19.7917C19.9437 19.74 20.0894 19.8 20.1969 19.907C20.3043 20.015 20.3647 20.161 20.3647 20.313Z"
|
||||
fill="#008836"
|
||||
/>
|
||||
</Svg>
|
||||
</Animated.View>
|
||||
|
||||
{/* Bottom-right — eye-off */}
|
||||
<Animated.View
|
||||
style={[{ position: "absolute", bottom: 0, right: 5 }, styleBR]}
|
||||
>
|
||||
<Svg width={43} height={43} viewBox="0 0 43 43" fill="none">
|
||||
<Rect
|
||||
x={0.25}
|
||||
y={0.25}
|
||||
width={42.5}
|
||||
height={42.5}
|
||||
rx={7.75}
|
||||
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
|
||||
stroke={isDark ? "#233C2D" : "#D3E8DB"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<G>
|
||||
<Path
|
||||
d="M15.028 3.408C14.969 3.341 14.898 3.287 14.818 3.248C14.738 3.21 14.651 3.187 14.562 3.182C14.473 3.178 14.384 3.191 14.301 3.22C14.217 3.25 14.14 3.296 14.074 3.356C14.008 3.416 13.955 3.488 13.917 3.569C13.88 3.649 13.858 3.737 13.855 3.825C13.851 3.914 13.865 4.003 13.896 4.086C13.926 4.17 13.973 4.246 14.034 4.311L15.65 6.089C12.6 7.961 11.288 10.847 11.23 10.978C11.192 11.064 11.172 11.157 11.172 11.251C11.172 11.345 11.192 11.438 11.23 11.524C11.259 11.59 11.971 13.167 13.552 14.749C15.659 16.855 18.321 17.969 21.25 17.969C22.756 17.977 24.246 17.667 25.623 17.059L27.471 19.092C27.53 19.159 27.601 19.213 27.681 19.252C27.761 19.29 27.848 19.313 27.937 19.317C28.026 19.322 28.115 19.309 28.199 19.28C28.282 19.25 28.359 19.204 28.425 19.144C28.491 19.084 28.544 19.012 28.582 18.931C28.62 18.851 28.641 18.763 28.645 18.675C28.648 18.586 28.634 18.497 28.604 18.414C28.573 18.33 28.526 18.254 28.465 18.189L15.028 3.408ZM19.003 9.777L22.503 13.628C21.976 13.905 21.372 13.999 20.785 13.896C20.199 13.793 19.663 13.497 19.263 13.057C18.862 12.616 18.62 12.055 18.573 11.461C18.526 10.868 18.677 10.275 19.003 9.777ZM21.25 16.625C18.665 16.625 16.407 15.685 14.537 13.833C13.77 13.07 13.117 12.2 12.6 11.25C12.993 10.512 14.251 8.446 16.576 7.103L18.088 8.762C17.503 9.511 17.201 10.443 17.237 11.394C17.273 12.344 17.643 13.251 18.283 13.955C18.923 14.658 19.791 15.113 20.733 15.238C21.676 15.364 22.633 15.152 23.434 14.64L24.672 16.001C23.58 16.42 22.419 16.631 21.25 16.625ZM21.754 8.61C21.579 8.577 21.424 8.475 21.324 8.328C21.224 8.181 21.186 7.999 21.22 7.824C21.253 7.649 21.355 7.495 21.502 7.394C21.65 7.294 21.831 7.257 22.006 7.29C22.862 7.456 23.642 7.895 24.229 8.54C24.815 9.185 25.178 10.003 25.262 10.871C25.279 11.049 25.224 11.225 25.11 11.363C24.997 11.5 24.833 11.586 24.656 11.603C24.635 11.604 24.614 11.604 24.593 11.603C24.425 11.603 24.263 11.541 24.138 11.428C24.014 11.316 23.936 11.16 23.921 10.993C23.864 10.416 23.623 9.872 23.232 9.442C22.842 9.013 22.323 8.721 21.754 8.61ZM31.268 11.524C31.232 11.603 30.382 13.486 28.466 15.202C28.4 15.263 28.324 15.31 28.24 15.34C28.156 15.371 28.067 15.384 27.978 15.38C27.889 15.376 27.802 15.354 27.721 15.316C27.641 15.277 27.569 15.223 27.509 15.157C27.45 15.091 27.404 15.013 27.375 14.929C27.346 14.845 27.334 14.755 27.339 14.666C27.345 14.577 27.368 14.491 27.408 14.411C27.447 14.331 27.502 14.26 27.57 14.201C28.51 13.357 29.299 12.359 29.905 11.25C29.386 10.299 28.732 9.429 27.963 8.666C26.093 6.815 23.835 5.875 21.25 5.875C20.705 5.874 20.162 5.918 19.624 6.007C19.537 6.022 19.447 6.02 19.36 6.001C19.274 5.982 19.192 5.945 19.119 5.894C19.047 5.843 18.985 5.778 18.938 5.703C18.891 5.627 18.859 5.544 18.844 5.456C18.83 5.369 18.833 5.279 18.853 5.193C18.873 5.106 18.91 5.025 18.962 4.953C19.014 4.88 19.079 4.819 19.155 4.773C19.231 4.726 19.315 4.695 19.402 4.682C20.013 4.581 20.631 4.53 21.25 4.531C24.179 4.531 26.841 5.645 28.948 7.752C30.529 9.333 31.241 10.912 31.27 10.978C31.308 11.064 31.328 11.157 31.328 11.251C31.328 11.345 31.308 11.438 31.27 11.524H31.268Z"
|
||||
fill="#008836"
|
||||
transform="translate(0, 10)"
|
||||
/>
|
||||
</G>
|
||||
</Svg>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
);
|
||||
};
|
||||
@@ -20,99 +20,92 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Linking, useWindowDimensions, View } from "react-native";
|
||||
import { SwiperFlatList } from "react-native-swiper-flatlist";
|
||||
import { Platform, useWindowDimensions, View } from "react-native";
|
||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import useRotator from "../../hooks/use-rotator";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { AuthMode } from "../auth/common";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { IntroIllustration } from "./illustration";
|
||||
import { ProgressPills } from "./progress-pills";
|
||||
|
||||
const Intro = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const { width } = useWindowDimensions();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const isTablet = width > 600;
|
||||
const rotator = useRotator([0], 10000, true);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({ item }: { item: (typeof strings.introData)[0] }) => (
|
||||
<View
|
||||
<Animated.View
|
||||
entering={FadeIn}
|
||||
exiting={FadeOut}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
width: isTablet ? width / 2 : width,
|
||||
paddingHorizontal: isTablet ? (width / 2) * 0.05 : width * 0.05
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
{item.headings?.map((heading, index) =>
|
||||
heading.bold ? (
|
||||
<Heading
|
||||
key={heading.value()}
|
||||
fontFamily="SEMI_BOLD"
|
||||
fontSize="XXL"
|
||||
>
|
||||
{heading.value()}
|
||||
</Heading>
|
||||
) : (
|
||||
<Paragraph
|
||||
key={heading.value()}
|
||||
style={{
|
||||
marginTop: index !== 0 ? -5 : undefined
|
||||
}}
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="XL"
|
||||
>
|
||||
{heading.value()}
|
||||
</Paragraph>
|
||||
)
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
maxWidth: "90%",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{item.headings?.map((heading) => (
|
||||
<Heading
|
||||
key={heading()}
|
||||
style={{
|
||||
marginBottom: 5
|
||||
}}
|
||||
extraBold
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{heading()}
|
||||
</Heading>
|
||||
))}
|
||||
{item.user ? (
|
||||
<Paragraph fontFamily="MEDIUM" fontSize="XL">
|
||||
{item.user}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{item.body ? (
|
||||
<Paragraph size={AppFontSize.sm}>{item.body()}</Paragraph>
|
||||
<Paragraph color={colors.primary.paragraph} fontSize="SM">
|
||||
{item.body()}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{item.tesimonial ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
fontSize: AppFontSize.lg
|
||||
}}
|
||||
fontSize="SM"
|
||||
color={colors.secondary.paragraph}
|
||||
onPress={() => {
|
||||
Linking.openURL(item.link);
|
||||
// Linking.openURL(item.link);
|
||||
}}
|
||||
>
|
||||
{item.tesimonial()} — {item.user}
|
||||
{item.tesimonial()}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
),
|
||||
[colors.primary.accent, colors.secondary.background, isTablet, width]
|
||||
[colors.primary.paragraph, colors.secondary.paragraph]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -126,17 +119,25 @@ const Intro = () => {
|
||||
<View
|
||||
testID="notesnook.splashscreen"
|
||||
style={{
|
||||
flex: 1
|
||||
flex: 1,
|
||||
paddingTop: Platform.OS === "android" ? 60 - insets.top : undefined
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<ProgressPills activePillIndex={0} />
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: "100%",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.top + 10,
|
||||
flexGrow: 1
|
||||
},
|
||||
isTablet && {
|
||||
@@ -149,33 +150,34 @@ const Intro = () => {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SwiperFlatList
|
||||
autoplay
|
||||
autoplayDelay={10}
|
||||
autoplayLoop={true}
|
||||
index={0}
|
||||
useReactNativeGestureHandler={true}
|
||||
showPagination
|
||||
data={strings.introData}
|
||||
paginationActiveColor={colors.primary.accent}
|
||||
paginationStyleItem={{
|
||||
width: 10,
|
||||
height: 5,
|
||||
marginRight: 4,
|
||||
marginLeft: 4
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
alignSelf: "center",
|
||||
paddingVertical: Spacing.LEVEL_7
|
||||
}}
|
||||
paginationDefaultColor={colors.primary.border}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
>
|
||||
<IntroIllustration />
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{strings.introData.map((item, index) =>
|
||||
index !== rotator ? null : renderItem({ item })
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: isTablet ? "50%" : "100%",
|
||||
justifyContent: "center",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: isTablet ? 0 : DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
gap: Spacing.LEVEL_2,
|
||||
padding: Spacing.LEVEL_3,
|
||||
flexShrink: 1,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
@@ -191,7 +193,7 @@ const Intro = () => {
|
||||
});
|
||||
}}
|
||||
type="accent"
|
||||
title={strings.getStarted()}
|
||||
title={strings.continue()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -19,39 +19,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { IconButton, IconButtonProps } from "../ui/icon-button";
|
||||
import { View } from "react-native";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
|
||||
export const RightMenus = ({
|
||||
rightButton
|
||||
}: {
|
||||
rightButton?: IconButtonProps;
|
||||
export const ProgressPills = (props: {
|
||||
activePillIndex: number;
|
||||
count?: number;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View style={styles.rightBtnContainer}>
|
||||
{rightButton ? (
|
||||
<IconButton {...rightButton} color={colors.primary.icon} />
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{new Array(props.count || 3).fill(0).map((item, index) => (
|
||||
<View
|
||||
key={`${item}-progress-pill-${props.count}`}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40
|
||||
width: props.activePillIndex === index ? 26 : 14,
|
||||
height: 5,
|
||||
backgroundColor:
|
||||
props.activePillIndex === index
|
||||
? colors.primary.accent
|
||||
: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
rightBtnContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
},
|
||||
rightBtn: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}
|
||||
});
|
||||
@@ -22,22 +22,22 @@ import {
|
||||
GroupingByIdKey,
|
||||
GroupingKey,
|
||||
GroupOptions,
|
||||
ItemType
|
||||
ItemType,
|
||||
Item,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import React, { RefObject } from "react";
|
||||
import { FlatList, View } from "react-native";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { RouteName } from "../../../stores/use-navigation-store";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import Sort from "../../sheets/sort";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
type SectionHeaderProps = {
|
||||
item: GroupHeader;
|
||||
@@ -49,8 +49,9 @@ type SectionHeaderProps = {
|
||||
group: GroupingKey;
|
||||
groupId?: string;
|
||||
type?: GroupingByIdKey;
|
||||
onOpenJumpToDialog: () => void;
|
||||
itemCount?: number;
|
||||
data?: VirtualizedGrouping<Item>;
|
||||
ref?: RefObject<FlatList>;
|
||||
};
|
||||
|
||||
export const SectionHeader = React.memo<
|
||||
@@ -64,12 +65,13 @@ export const SectionHeader = React.memo<
|
||||
screen,
|
||||
groupOptions,
|
||||
group,
|
||||
onOpenJumpToDialog,
|
||||
itemCount,
|
||||
groupId,
|
||||
type
|
||||
type,
|
||||
data,
|
||||
ref
|
||||
}: SectionHeaderProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const { colors } = useThemeColors("list");
|
||||
const isCompactModeEnabled = useIsCompactModeEnabled(
|
||||
dataType as "note" | "notebook" | "searchResult"
|
||||
);
|
||||
@@ -78,8 +80,9 @@ export const SectionHeader = React.memo<
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
marginBottom: Spacing.LEVEL_3,
|
||||
marginTop: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -88,58 +91,36 @@ export const SectionHeader = React.memo<
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
justifyContent: "space-between",
|
||||
borderBottomWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
paddingBottom: 1,
|
||||
paddingTop:
|
||||
index === 0
|
||||
? DefaultAppStyles.GAP_VERTICAL
|
||||
: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
onOpenJumpToDialog();
|
||||
}}
|
||||
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
|
||||
<Paragraph
|
||||
fontSize="MD"
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
flexDirection: "row",
|
||||
width: "auto"
|
||||
alignSelf: "center",
|
||||
textAlignVertical: "center"
|
||||
}}
|
||||
color={colors.secondary.heading}
|
||||
>
|
||||
<Heading
|
||||
size={AppFontSize.xxs}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlignVertical: "center"
|
||||
}}
|
||||
color={color || colors.primary.accent}
|
||||
>
|
||||
{!item.title || item.title === ""
|
||||
? screen === "Search"
|
||||
? strings.results(itemCount || 0)
|
||||
: strings.pinned().toUpperCase()
|
||||
: item.title.toUpperCase()}
|
||||
</Heading>
|
||||
</Pressable>
|
||||
{!item.title || item.title === ""
|
||||
? screen === "Search"
|
||||
? strings.results(itemCount || 0)
|
||||
: strings.pinned()
|
||||
: item.title}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{index === 0 ? (
|
||||
<>
|
||||
<IconButton
|
||||
name={
|
||||
groupOptions.sortDirection === "asc"
|
||||
? "sort-ascending"
|
||||
: "sort-descending"
|
||||
}
|
||||
name={"sliders"}
|
||||
iconFamily="notesnook"
|
||||
color={colors.secondary.icon}
|
||||
testID="icon-sort"
|
||||
onPress={() => {
|
||||
@@ -155,15 +136,20 @@ export const SectionHeader = React.memo<
|
||||
hideGroupOptions={
|
||||
screen === "Reminders" || screen === "Search"
|
||||
}
|
||||
data={data}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderWidth: 1,
|
||||
borderRadius: Radius.XS,
|
||||
borderColor: colors.secondary.border
|
||||
}}
|
||||
size={AppFontSize.lg - 2}
|
||||
size={16}
|
||||
/>
|
||||
<IconButton
|
||||
hidden={
|
||||
@@ -173,14 +159,16 @@ export const SectionHeader = React.memo<
|
||||
screen !== "Search"
|
||||
}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderWidth: 1,
|
||||
borderRadius: Radius.XXS,
|
||||
borderColor: colors.secondary.border
|
||||
}}
|
||||
testID="icon-compact-mode"
|
||||
color={colors.secondary.icon}
|
||||
name={
|
||||
isCompactModeEnabled ? "view-list" : "view-list-outline"
|
||||
}
|
||||
name={"view-list"}
|
||||
iconFamily="notesnook"
|
||||
onPress={() => {
|
||||
SettingsService.set({
|
||||
[dataType === "notebook"
|
||||
@@ -192,20 +180,10 @@ export const SectionHeader = React.memo<
|
||||
: "normal"
|
||||
});
|
||||
}}
|
||||
size={AppFontSize.lg - 2}
|
||||
size={16}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* <IconButton
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25
|
||||
}}
|
||||
color={colors.secondary.icon}
|
||||
name={"chevron-down"}
|
||||
size={SIZE.lg - 2}
|
||||
/> */}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import { EntityLevel, decode } from "entities";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import useNavigationStore, {
|
||||
RouteParams
|
||||
@@ -56,6 +55,9 @@ import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import dayjs from "dayjs";
|
||||
import { ExpiryDate } from "../../ui/expiry-date";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { create } from "zustand";
|
||||
import { FontFamily } from "../../../common/design/font";
|
||||
|
||||
type NoteItemProps = {
|
||||
item: Note | BaseTrashItem<Note>;
|
||||
@@ -72,6 +74,24 @@ type NoteItemProps = {
|
||||
renderedInRoute?: keyof RouteParams;
|
||||
};
|
||||
|
||||
const useShowMoreStore = create<{
|
||||
showMoreStatus: Record<string, boolean>;
|
||||
show: (id: string) => void;
|
||||
hide: (id: string) => void;
|
||||
}>((set) => ({
|
||||
showMoreStatus: {},
|
||||
show(id) {
|
||||
set((state) => ({
|
||||
showMoreStatus: { ...state.showMoreStatus, [id]: true }
|
||||
}));
|
||||
},
|
||||
hide(id) {
|
||||
set((state) => ({
|
||||
showMoreStatus: { ...state.showMoreStatus, [id]: false }
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
const NoteItem = ({
|
||||
item,
|
||||
isTrash,
|
||||
@@ -98,68 +118,54 @@ const NoteItem = ({
|
||||
const primaryColors = isEditingNote ? colors.selected : colors.primary;
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const [selected] = useIsSelected(item);
|
||||
const showMore = useShowMoreStore((state) => state.showMoreStatus[item.id]);
|
||||
const statusIcons = [
|
||||
{
|
||||
condition: item.conflicted,
|
||||
name: "alert-circle",
|
||||
color: colors.error.accent
|
||||
},
|
||||
{
|
||||
condition: item.localOnly,
|
||||
testID: "sync-off",
|
||||
name: "sync-off",
|
||||
color: primaryColors.icon
|
||||
},
|
||||
{
|
||||
condition: item.readonly,
|
||||
testID: "pencil-lock",
|
||||
name: "pencil-lock",
|
||||
color: primaryColors.icon
|
||||
},
|
||||
{
|
||||
condition: item.pinned,
|
||||
testID: "icon-pinned",
|
||||
name: "pin",
|
||||
color: primaryColors.icon
|
||||
},
|
||||
{
|
||||
condition: !!locked,
|
||||
testID: "lock",
|
||||
name: "lock",
|
||||
color: primaryColors.icon
|
||||
},
|
||||
{
|
||||
condition: item.favorite,
|
||||
testID: "star-filled",
|
||||
name: "star-filled",
|
||||
color: "orange"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1
|
||||
flexShrink: 1,
|
||||
gap: compactMode ? undefined : Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{compactMode ? null : (
|
||||
<Paragraph
|
||||
style={{
|
||||
fontSize: AppFontSize.xxxs,
|
||||
color: colors.secondary.paragraph
|
||||
}}
|
||||
>
|
||||
{getFormattedDate(
|
||||
date,
|
||||
dayjs(date).isBefore(dayjs().subtract(1, "day").hour(23))
|
||||
? "date"
|
||||
: "time"
|
||||
)}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{compactMode ? (
|
||||
<Paragraph
|
||||
numberOfLines={1}
|
||||
color={color?.colorCode || primaryColors.heading}
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
paddingRight: 10
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
color={color?.colorCode || primaryColors.heading}
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
paddingRight: 10
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{item.headline && !compactMode ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
color={primaryColors.paragraph}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{decode(item.headline, {
|
||||
level: EntityLevel.HTML
|
||||
})}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{compactMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
@@ -167,149 +173,89 @@ const NoteItem = ({
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
columnGap: 8,
|
||||
rowGap: 4,
|
||||
columnGap: Spacing.LEVEL_0,
|
||||
rowGap: Spacing.LEVEL_0,
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{!isTrash ? (
|
||||
<>
|
||||
{item.conflicted ? (
|
||||
<Icon
|
||||
name="alert-circle"
|
||||
size={AppFontSize.sm}
|
||||
color={colors.error.accent}
|
||||
/>
|
||||
) : null}
|
||||
{statusIcons
|
||||
.filter((statusIcon) => statusIcon.condition)
|
||||
.map((statusIcon) => (
|
||||
<View
|
||||
key={statusIcon.testID || statusIcon.name}
|
||||
style={{
|
||||
borderRadius: Radius.XXS,
|
||||
paddingHorizontal: 3,
|
||||
paddingVertical: 3,
|
||||
// borderWidth: 1,
|
||||
// borderColor: primaryColors.border,
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
testID={statusIcon.testID}
|
||||
name={statusIcon.name}
|
||||
size={12}
|
||||
iconFamily="notesnook"
|
||||
color={statusIcon.color}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{item.localOnly ? (
|
||||
<Icon
|
||||
testID="sync-off"
|
||||
name="sync-off"
|
||||
size={AppFontSize.sm}
|
||||
color={primaryColors.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.readonly ? (
|
||||
<Icon
|
||||
testID="pencil-lock"
|
||||
name="pencil-lock"
|
||||
size={AppFontSize.sm}
|
||||
color={primaryColors.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{attachmentsCount > 0 ? (
|
||||
{attachmentsCount !== 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 2
|
||||
borderRadius: Radius.XXS,
|
||||
paddingHorizontal: 3,
|
||||
paddingVertical: 3,
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="attachment"
|
||||
size={AppFontSize.sm}
|
||||
color={primaryColors.icon}
|
||||
<AppIcon
|
||||
name="link"
|
||||
iconFamily="notesnook"
|
||||
size={12}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xxs}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph} fontSize="XS">
|
||||
{attachmentsCount}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{item.pinned ? (
|
||||
<Icon
|
||||
testID="icon-pinned"
|
||||
name="pin-outline"
|
||||
size={AppFontSize.sm}
|
||||
color={color?.colorCode || primaryColors.accent}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{locked ? (
|
||||
<Icon
|
||||
name="lock"
|
||||
testID="note-locked-icon"
|
||||
size={AppFontSize.sm}
|
||||
color={primaryColors.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.favorite ? (
|
||||
<Icon
|
||||
testID="icon-star"
|
||||
name="star-outline"
|
||||
size={AppFontSize.sm}
|
||||
color="orange"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{reminder ? (
|
||||
<ReminderTime
|
||||
reminder={reminder}
|
||||
color={color?.colorCode}
|
||||
textStyle={{
|
||||
fontSize: AppFontSize.xxs
|
||||
}}
|
||||
short
|
||||
iconSize={AppFontSize.xxs}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.expiryDate?.value ? (
|
||||
<ExpiryDate
|
||||
note={item as Note}
|
||||
color={color?.colorCode}
|
||||
textStyle={{ fontSize: AppFontSize.xxs }}
|
||||
short
|
||||
iconSize={AppFontSize.xxs}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{notebooks?.items
|
||||
?.filter(
|
||||
(item) =>
|
||||
renderedInRoute !== "Notebook" ||
|
||||
item.id !== useNavigationStore.getState().focusedRouteId
|
||||
)
|
||||
.filter((_, index) => showMore || index < 1)
|
||||
.map((item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
paddingVertical: 2,
|
||||
backgroundColor: colors.secondary.background,
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL / 2,
|
||||
borderWidth: 0.5,
|
||||
borderColor: primaryColors.border,
|
||||
paddingVertical: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL / 2
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="book-outline"
|
||||
size={AppFontSize.xxxs}
|
||||
name="bookmark"
|
||||
iconFamily="notesnook"
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<Paragraph
|
||||
size={AppFontSize.xxxs}
|
||||
fontSize="XS"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{item.title}
|
||||
@@ -317,31 +263,64 @@ const NoteItem = ({
|
||||
</View>
|
||||
))}
|
||||
|
||||
{!isTrash && !compactMode && tags
|
||||
? tags.items?.map((item) =>
|
||||
item.id ? (
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.secondary.background,
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL / 2,
|
||||
borderWidth: 0.5,
|
||||
borderColor:
|
||||
color?.colorCode || primaryColors.border,
|
||||
paddingVertical: 1
|
||||
}}
|
||||
{tags?.items
|
||||
?.filter((_, index) => showMore || index < 1)
|
||||
.map((item) =>
|
||||
item.id ? (
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
paddingVertical: 2,
|
||||
backgroundColor: colors.secondary.background,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xxxs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
#{item.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null
|
||||
)
|
||||
: null}
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null
|
||||
)}
|
||||
|
||||
{(() => {
|
||||
const filteredNotebooks = (notebooks?.items || []).filter(
|
||||
(nb) =>
|
||||
renderedInRoute !== "Notebook" ||
|
||||
nb.id !== useNavigationStore.getState().focusedRouteId
|
||||
);
|
||||
const filteredTags = (tags?.items || []).filter((t) => t.id);
|
||||
const totalNotebooks = filteredNotebooks.length;
|
||||
const totalTags = filteredTags.length;
|
||||
const hasMore = totalNotebooks > 1 || totalTags > 1;
|
||||
if (!hasMore) return null;
|
||||
const hiddenCount =
|
||||
(totalNotebooks > 1 ? totalNotebooks - 1 : 0) +
|
||||
(totalTags > 1 ? totalTags - 1 : 0);
|
||||
return (
|
||||
<Heading
|
||||
size={AppFontSize.xs}
|
||||
color={colors.primary.accent}
|
||||
onPress={() => {
|
||||
if (showMore) {
|
||||
useShowMoreStore.getState().hide(item.id);
|
||||
} else {
|
||||
useShowMoreStore.getState().show(item.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{showMore
|
||||
? "Show less"
|
||||
: `+${hiddenCount} ${strings.more()}`}
|
||||
</Heading>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -372,6 +351,207 @@ const NoteItem = ({
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{color && !compactMode ? (
|
||||
<View
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 100,
|
||||
backgroundColor: color?.colorCode
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
flexGrow: 1,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
color={primaryColors.heading}
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{color ? (
|
||||
<View
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 100,
|
||||
backgroundColor: color?.colorCode
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{compactMode ? (
|
||||
<>
|
||||
{item.conflicted ? (
|
||||
<AppIcon
|
||||
name="warning-circle"
|
||||
size={12}
|
||||
iconFamily="notesnook"
|
||||
color={colors.error.accent}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{locked ? (
|
||||
<AppIcon
|
||||
name="lock"
|
||||
testID="lock"
|
||||
iconFamily="notesnook"
|
||||
size={12}
|
||||
color={primaryColors.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.favorite ? (
|
||||
<AppIcon
|
||||
testID="icon-star"
|
||||
name="star-filled"
|
||||
iconFamily="notesnook"
|
||||
size={12}
|
||||
color={colors.static.orange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TimeSince
|
||||
style={{
|
||||
fontSize: AppFontSize.xs,
|
||||
color: colors.secondary.paragraph,
|
||||
marginRight: 6
|
||||
}}
|
||||
time={date}
|
||||
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
testID={notesnook.listitem.menu}
|
||||
color={colors.secondary.icon}
|
||||
name="dots-three"
|
||||
iconFamily="notesnook"
|
||||
size={20}
|
||||
onPress={() => !noOpen && Properties.present(item)}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
height: undefined,
|
||||
width: undefined,
|
||||
borderRadius: 100,
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{item.headline && !compactMode ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
color={primaryColors.paragraph}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{decode(item.headline, {
|
||||
level: EntityLevel.HTML
|
||||
})}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{compactMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_0 + 2,
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.secondary.icon}
|
||||
size={13}
|
||||
name="calendar"
|
||||
iconFamily="notesnook"
|
||||
/>
|
||||
<Paragraph
|
||||
style={{
|
||||
fontSize: AppFontSize.xs,
|
||||
color: colors.secondary.paragraph
|
||||
}}
|
||||
>
|
||||
{getFormattedDate(
|
||||
date,
|
||||
dayjs(date).isBefore(dayjs().subtract(1, "day").hour(23))
|
||||
? "date"
|
||||
: "time"
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{item.expiryDate?.value ? (
|
||||
<ExpiryDate
|
||||
note={item as Note}
|
||||
color={color?.colorCode}
|
||||
textStyle={{ fontSize: AppFontSize.xxs }}
|
||||
short
|
||||
iconSize={AppFontSize.xxs}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{reminder ? (
|
||||
<ReminderTime
|
||||
reminder={reminder}
|
||||
color={color?.colorCode}
|
||||
textStyle={{
|
||||
fontSize: AppFontSize.xs,
|
||||
fontFamily: FontFamily.REGULAR
|
||||
}}
|
||||
short
|
||||
iconSize={AppFontSize.xxs}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
|
||||
alignSelf: "flex-start",
|
||||
backgroundColor: colors.primary.shade
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
@@ -379,55 +559,6 @@ const NoteItem = ({
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{compactMode ? (
|
||||
<>
|
||||
{item.conflicted ? (
|
||||
<Icon
|
||||
name="alert-circle"
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
size={AppFontSize.sm}
|
||||
color={colors.error.accent}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{locked ? (
|
||||
<Icon
|
||||
name="lock"
|
||||
testID="note-locked-icon"
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={primaryColors.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.favorite ? (
|
||||
<Icon
|
||||
testID="icon-star"
|
||||
name="star-outline"
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={colors.static.orange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TimeSince
|
||||
style={{
|
||||
fontSize: AppFontSize.xxs,
|
||||
color: colors.secondary.paragraph,
|
||||
marginRight: 6
|
||||
}}
|
||||
time={date}
|
||||
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{selectionMode === "note" || selectionMode === "trash" ? (
|
||||
<>
|
||||
<View
|
||||
@@ -445,22 +576,7 @@ const NoteItem = ({
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<IconButton
|
||||
testID={notesnook.listitem.menu}
|
||||
color={colors.secondary.icon}
|
||||
name="dots-horizontal"
|
||||
size={AppFontSize.lg}
|
||||
onPress={() => !noOpen && Properties.present(item)}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
width: 35,
|
||||
borderRadius: 100,
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -36,7 +36,8 @@ import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
import { RouteParams } from "../../../stores/use-navigation-store";
|
||||
import NotePreview from "../../note-history/preview";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
import { selectItem } from "../../../stores/use-selection-store";
|
||||
|
||||
export const openNote = async (
|
||||
item: Note,
|
||||
@@ -94,6 +95,7 @@ type NoteWrapperProps = {
|
||||
isRenderedInActionSheet: boolean;
|
||||
locked?: boolean;
|
||||
renderedInRoute?: keyof RouteParams;
|
||||
hasGroupHeader?: boolean;
|
||||
};
|
||||
|
||||
export const NoteWrapper = React.memo<
|
||||
@@ -103,6 +105,7 @@ export const NoteWrapper = React.memo<
|
||||
item,
|
||||
index,
|
||||
isRenderedInActionSheet,
|
||||
hasGroupHeader,
|
||||
...restProps
|
||||
}: NoteWrapperProps) {
|
||||
const isTrash = item.type === "trash";
|
||||
@@ -113,6 +116,7 @@ export const NoteWrapper = React.memo<
|
||||
onPress={() => openNote(item as Note, isTrash, isRenderedInActionSheet)}
|
||||
isSheet={isRenderedInActionSheet}
|
||||
item={item}
|
||||
hasGroupHeader={hasGroupHeader}
|
||||
index={index}
|
||||
color={restProps.color?.colorCode}
|
||||
>
|
||||
|
||||
@@ -27,7 +27,8 @@ import Navigation from "../../../services/navigation";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useTrashStore } from "../../../stores/use-trash-store";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
import { selectItem } from "../../../stores/use-selection-store";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
|
||||
@@ -36,7 +36,8 @@ import { IconButton } from "../../ui/icon-button";
|
||||
import { ReminderTime } from "../../ui/reminder-time";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
import { selectItem } from "../../../stores/use-selection-store";
|
||||
|
||||
const ReminderItem = React.memo(
|
||||
({
|
||||
|
||||
@@ -23,26 +23,9 @@ import React, { PropsWithChildren, useRef } from "react";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import { View } from "react-native";
|
||||
|
||||
export function selectItem(item: Item) {
|
||||
if (useSelectionStore.getState().selectionMode === item.type) {
|
||||
const { selectionMode, clearSelection, setSelectedItem } =
|
||||
useSelectionStore.getState();
|
||||
|
||||
if (selectionMode === item.type) {
|
||||
setSelectedItem(item.id);
|
||||
}
|
||||
|
||||
if (useSelectionStore.getState().selectedItemsList.length === 0) {
|
||||
clearSelection();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
import { Spacing } from "../../../common/design/spacing";
|
||||
|
||||
type SelectionWrapperProps = PropsWithChildren<{
|
||||
item: Item;
|
||||
@@ -51,6 +34,7 @@ type SelectionWrapperProps = PropsWithChildren<{
|
||||
isSheet?: boolean;
|
||||
color?: string;
|
||||
index?: number;
|
||||
hasGroupHeader?: boolean;
|
||||
}>;
|
||||
|
||||
const SelectionWrapper = ({
|
||||
@@ -60,6 +44,7 @@ const SelectionWrapper = ({
|
||||
isSheet,
|
||||
children,
|
||||
color,
|
||||
hasGroupHeader,
|
||||
index = 0
|
||||
}: SelectionWrapperProps) => {
|
||||
const itemId = useRef(item.id);
|
||||
@@ -86,47 +71,50 @@ const SelectionWrapper = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
customColor={
|
||||
isEditingNote
|
||||
? colors.selected.background
|
||||
: isSheet
|
||||
? colors.primary.hover
|
||||
: "transparent"
|
||||
}
|
||||
testID={testID}
|
||||
onLongPress={onLongPress}
|
||||
onPress={onPress}
|
||||
customSelectedColor={colors.primary.hover}
|
||||
customAlpha={!isDark ? -0.02 : 0.02}
|
||||
customOpacity={1}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
overflow: "hidden",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: compactMode ? 4 : DefaultAppStyles.GAP_VERTICAL,
|
||||
borderRadius: isSheet ? 10 : 0,
|
||||
marginBottom: isSheet ? DefaultAppStyles.GAP_VERTICAL : undefined
|
||||
// paddingHorizontal: Spacing.LEVEL_3,
|
||||
backgroundColor: isEditingNote ? colors.selected.background : undefined
|
||||
}}
|
||||
>
|
||||
{isEditingNote ? (
|
||||
<Pressable
|
||||
customColor={isSheet ? colors.primary.hover : "transparent"}
|
||||
testID={testID}
|
||||
onLongPress={onLongPress}
|
||||
onPress={onPress}
|
||||
customSelectedColor={colors.primary.hover}
|
||||
noborder
|
||||
customAlpha={!isDark ? -0.02 : 0.02}
|
||||
customOpacity={1}
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
overflow: "hidden",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
borderRadius: 0,
|
||||
alignItems: "flex-start",
|
||||
// paddingVertical: Spacing.LEVEL_2,
|
||||
paddingTop: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: color || colors.selected.accent,
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 5
|
||||
width: "100%",
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_2,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
width: "100%"
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{children}
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ import { Properties } from "../../properties";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
import { selectItem } from "../../../stores/use-selection-store";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
|
||||
@@ -21,10 +21,11 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import { Message, useMessageStore } from "../../stores/use-message-store";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const Card = ({
|
||||
@@ -47,15 +48,19 @@ export const Card = ({
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
marginBottom: Spacing.LEVEL_4,
|
||||
marginTop: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={messageBoardState.onPress}
|
||||
type="plain"
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: Spacing.LEVEL_3,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
backgroundColor: colors.primary.shade,
|
||||
borderRadius: Radius.S,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
@@ -66,23 +71,24 @@ export const Card = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
flexShrink: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale,
|
||||
borderRadius: 100,
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: Radius.XXS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={AppFontSize.xxxl}
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.error.icon : color
|
||||
}
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
allowFontScaling
|
||||
name={messageBoardState.icon}
|
||||
/>
|
||||
@@ -90,25 +96,27 @@ export const Card = ({
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
marginRight: 10
|
||||
gap: Spacing.LEVEL_0,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
<Heading
|
||||
style={{
|
||||
flexWrap: "nowrap",
|
||||
flexShrink: 1
|
||||
}}
|
||||
size={AppFontSize.sm}
|
||||
fontSize="MD"
|
||||
color={colors.primary.heading}
|
||||
>
|
||||
{messageBoardState.actionText}
|
||||
</Paragraph>
|
||||
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
|
||||
</Heading>
|
||||
<Paragraph color={colors.secondary.paragraph} fontSize="SM">
|
||||
{messageBoardState.message}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<AppIcon size={24} name="chevron-right" color={colors.primary.icon} />
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -21,13 +21,10 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { TTip, useTip } from "../../services/tip-manager";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { RouteParams } from "../../stores/use-navigation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Tip } from "../tip";
|
||||
import { Button } from "../ui/button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
@@ -58,22 +55,13 @@ export const Empty = React.memo(function Empty({
|
||||
screen
|
||||
}: EmptyListProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const tip = useTip(
|
||||
screen === "Notes" && introCompleted
|
||||
? "first-note"
|
||||
: placeholder?.type || ((dataType + "s") as any),
|
||||
screen === "Notes" ? "notes" : "list"
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flex: 1,
|
||||
width: "80%",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center"
|
||||
}
|
||||
@@ -81,50 +69,53 @@ export const Empty = React.memo(function Empty({
|
||||
>
|
||||
{!loading ? (
|
||||
<>
|
||||
<Tip
|
||||
color={color}
|
||||
tip={
|
||||
screen !== "Search"
|
||||
? tip || ({ text: () => placeholder?.paragraph } as TTip)
|
||||
: ({ text: () => placeholder?.paragraph } as TTip)
|
||||
}
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
/>
|
||||
{placeholder?.button && (
|
||||
<Button
|
||||
testID={notesnook.buttons.add}
|
||||
type="secondaryAccented"
|
||||
title={placeholder?.button}
|
||||
iconPosition="right"
|
||||
icon="arrow-right"
|
||||
onPress={placeholder?.action}
|
||||
>
|
||||
<Heading size={AppFontSize.md} color={colors.primary.heading}>
|
||||
{placeholder?.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "flex-start"
|
||||
textAlign: "center",
|
||||
marginBottom: Spacing.LEVEL_2,
|
||||
maxWidth: "80%"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
fontSize="SM"
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{placeholder?.paragraph}
|
||||
</Paragraph>
|
||||
|
||||
{placeholder?.button && (
|
||||
<Button
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
title={placeholder?.button}
|
||||
iconPosition="right"
|
||||
onPress={placeholder?.action}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
alignItems: "flex-start",
|
||||
width: "100%"
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Heading>{placeholder?.title}</Heading>
|
||||
<Paragraph size={AppFontSize.sm} textBreakStrategy="balanced">
|
||||
{placeholder?.loading}
|
||||
</Paragraph>
|
||||
<Seperator />
|
||||
<ActivityIndicator
|
||||
size={AppFontSize.lg}
|
||||
color={color || colors.primary.accent}
|
||||
/>
|
||||
<Paragraph>{placeholder?.loading}</Paragraph>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -43,9 +43,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { getGroupOptions } from "../../hooks/use-group-options";
|
||||
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { RouteName } from "../../stores/use-navigation-store";
|
||||
import { eOpenJumpToDialog } from "../../utils/events";
|
||||
import { SectionHeader } from "../list-items/headers/section-header";
|
||||
import { NoteWrapper } from "../list-items/note/wrapper";
|
||||
import { NotebookWrapper } from "../list-items/notebook/wrapper";
|
||||
@@ -190,12 +188,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
type={props.type}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
onOpenJumpToDialog={() => {
|
||||
eSendEvent(eOpenJumpToDialog, {
|
||||
ref: props.scrollRef,
|
||||
data: items
|
||||
});
|
||||
}}
|
||||
ref={props.scrollRef}
|
||||
data={items}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -205,6 +199,9 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
color={color.current}
|
||||
notebooks={notebooks.current}
|
||||
reminder={reminder.current}
|
||||
hasGroupHeader={
|
||||
groupHeader && previousIndex.current === index && !isSheet
|
||||
}
|
||||
attachmentsCount={attachmentsCount.current}
|
||||
date={getDate(item as Note, group)}
|
||||
isRenderedInActionSheet={isSheet}
|
||||
@@ -229,12 +226,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
type={props.type}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
onOpenJumpToDialog={() => {
|
||||
eSendEvent(eOpenJumpToDialog, {
|
||||
ref: props.scrollRef,
|
||||
data: items
|
||||
});
|
||||
}}
|
||||
ref={props.scrollRef}
|
||||
data={items}
|
||||
/>
|
||||
) : null}
|
||||
<NotebookWrapper
|
||||
@@ -260,12 +253,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
type={props.type}
|
||||
groupId={props.groupId}
|
||||
groupOptions={groupOptions}
|
||||
onOpenJumpToDialog={() => {
|
||||
eSendEvent(eOpenJumpToDialog, {
|
||||
ref: props.scrollRef,
|
||||
data: items
|
||||
});
|
||||
}}
|
||||
ref={props.scrollRef}
|
||||
data={items}
|
||||
/>
|
||||
) : null}
|
||||
<ReminderItem
|
||||
@@ -289,12 +278,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
type={props.type}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
onOpenJumpToDialog={() => {
|
||||
eSendEvent(eOpenJumpToDialog, {
|
||||
ref: props.scrollRef,
|
||||
data: items
|
||||
});
|
||||
}}
|
||||
ref={props.scrollRef}
|
||||
data={items}
|
||||
/>
|
||||
) : null}
|
||||
<TagItem
|
||||
@@ -319,12 +304,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
itemCount={items?.placeholders.length}
|
||||
onOpenJumpToDialog={() => {
|
||||
eSendEvent(eOpenJumpToDialog, {
|
||||
ref: props.scrollRef,
|
||||
data: items
|
||||
});
|
||||
}}
|
||||
ref={props.scrollRef}
|
||||
data={items}
|
||||
/>
|
||||
) : null}
|
||||
<SearchResult item={item as HighlightedResult} />
|
||||
|
||||
@@ -18,15 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { View, ViewStyle } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
export const Loading = (props: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
svgSrc?: string;
|
||||
style?: ViewStyle;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
@@ -37,18 +41,38 @@ export const Loading = (props: {
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
...props.style
|
||||
}}
|
||||
>
|
||||
{props.icon ? (
|
||||
<Icon name={props.icon} size={80} color={colors.primary.accent} />
|
||||
) : null}
|
||||
|
||||
{props.svgSrc ? (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: Spacing.LEVEL_7
|
||||
}}
|
||||
>
|
||||
<SvgView
|
||||
src={props.svgSrc}
|
||||
style={{
|
||||
width: 180,
|
||||
height: 180
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{props.title ? (
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
textAlign: "center",
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
fontSize="XL"
|
||||
lineHeight={undefined}
|
||||
>
|
||||
{props.title}
|
||||
</Heading>
|
||||
@@ -57,8 +81,10 @@ export const Loading = (props: {
|
||||
{props.description ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
textAlign: "center",
|
||||
marginBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
fontSize="SM"
|
||||
>
|
||||
{props.description}
|
||||
</Paragraph>
|
||||
@@ -67,15 +93,15 @@ export const Loading = (props: {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: 15
|
||||
width: 300
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
height={7}
|
||||
width={300}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
borderRadius={Radius.XS}
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
|
||||
24
apps/mobile/app/components/paywall/common.ts
Normal file
24
apps/mobile/app/components/paywall/common.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
export const Steps = {
|
||||
select: 1,
|
||||
buy: 2,
|
||||
finish: 3,
|
||||
buyWeb: 4
|
||||
};
|
||||
185
apps/mobile/app/components/paywall/compare-plans.tsx
Normal file
185
apps/mobile/app/components/paywall/compare-plans.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
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 { getFeaturesTable } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { ScrollView, useWindowDimensions, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
//@ts-ignore
|
||||
import usePricingPlans from "../../hooks/use-pricing-plans";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Steps } from "./common";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
|
||||
export const ComparePlans = React.memo(
|
||||
(props: {
|
||||
pricingPlans?: ReturnType<typeof usePricingPlans>;
|
||||
setStep: (step: number) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { width } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={{
|
||||
width: isTablet ? "100%" : undefined,
|
||||
paddingLeft: Spacing.LEVEL_3
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
{["Features", "Free", "Essential", "Pro", "Believer"].map(
|
||||
(plan, index) => (
|
||||
<View
|
||||
key={plan}
|
||||
style={{
|
||||
width: index === 0 ? 150 : 120,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
backgroundColor:
|
||||
index === 0 ? colors.secondary.background : undefined,
|
||||
borderBottomWidth: index === 0 ? 1 : undefined,
|
||||
borderBottomColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.sm}>{plan}</Heading>
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
|
||||
{getFeaturesTable().map((item, keyIndex) => {
|
||||
return (
|
||||
<View
|
||||
key={`${item[0] + item[1]}`}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
{item.map((featureItem, index) => (
|
||||
<View
|
||||
style={{
|
||||
width: index === 0 ? 150 : 120,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
backgroundColor:
|
||||
index === 0 ? colors.secondary.background : undefined,
|
||||
borderBottomWidth: index === 0 ? 1 : undefined,
|
||||
borderBottomColor: colors.primary.border
|
||||
}}
|
||||
key={item[0] + index}
|
||||
>
|
||||
{typeof featureItem === "string" ? (
|
||||
<Heading size={AppFontSize.sm}>
|
||||
{featureItem as string}
|
||||
</Heading>
|
||||
) : (
|
||||
<>
|
||||
{typeof featureItem.caption === "string" ||
|
||||
typeof featureItem.caption === "number" ? (
|
||||
<Paragraph>
|
||||
{featureItem.caption === "infinity"
|
||||
? "∞"
|
||||
: featureItem.caption}
|
||||
</Paragraph>
|
||||
) : typeof featureItem.caption === "boolean" ? (
|
||||
<>
|
||||
{featureItem.caption === true ? (
|
||||
<Icon
|
||||
color={colors.primary.accent}
|
||||
size={AppFontSize.sm}
|
||||
name="check"
|
||||
/>
|
||||
) : (
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
-
|
||||
</Paragraph>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
{["features", "free", "essential", "pro", "believer"].map(
|
||||
(plan, index) => (
|
||||
<View
|
||||
key={plan + "btn"}
|
||||
style={{
|
||||
width: index === 0 ? 150 : 120,
|
||||
paddingHorizontal: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{plan !== "free" && plan !== "features" ? (
|
||||
<Button
|
||||
title={strings.select()}
|
||||
type="accent"
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
borderRadius: Radius.XS,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
onPress={() => {
|
||||
props.pricingPlans?.selectPlan(plan);
|
||||
props.setStep(Steps.buy);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
},
|
||||
() => true
|
||||
);
|
||||
ComparePlans.displayName = "ComparePlans";
|
||||
73
apps/mobile/app/components/paywall/faq-item.tsx
Normal file
73
apps/mobile/app/components/paywall/faq-item.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
//@ts-ignore
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const FAQItem = (props: { question: string; answer: string }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
padding: 16,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 10,
|
||||
marginBottom: 10,
|
||||
gap: 12
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
key={props.question}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
size={AppFontSize.md}
|
||||
>
|
||||
{props.question}
|
||||
</Heading>
|
||||
<Icon
|
||||
name={expanded ? "chevron-up" : "chevron-down"}
|
||||
color={colors.secondary.icon}
|
||||
size={AppFontSize.xxl}
|
||||
/>
|
||||
</View>
|
||||
{expanded ? (
|
||||
<Paragraph size={AppFontSize.md}>{props.answer}</Paragraph>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
315
apps/mobile/app/components/paywall/plan-card.tsx
Normal file
315
apps/mobile/app/components/paywall/plan-card.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Text,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
import * as RNIap from "react-native-iap";
|
||||
//@ts-ignore
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import usePricingPlans, {
|
||||
PlanOverView,
|
||||
PricingPlan
|
||||
} from "../../hooks/use-pricing-plans";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Steps } from "./common";
|
||||
|
||||
export const PricingPlanCard = ({
|
||||
plan,
|
||||
pricingPlans,
|
||||
annualBilling,
|
||||
setStep
|
||||
}: {
|
||||
plan: PricingPlan;
|
||||
pricingPlans?: ReturnType<typeof usePricingPlans>;
|
||||
annualBilling?: boolean;
|
||||
setStep: (step: number) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const regionalDiscount =
|
||||
plan.id === "pro" && annualBilling
|
||||
? pricingPlans?.regionalDiscount
|
||||
: undefined;
|
||||
const { width } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
|
||||
const product =
|
||||
plan.subscriptions?.[
|
||||
regionalDiscount?.sku ||
|
||||
`notesnook.${plan.id}.${annualBilling ? "yearly" : "monthly"}`
|
||||
];
|
||||
|
||||
const WebPlan = pricingPlans?.getWebPlan(
|
||||
plan.id,
|
||||
annualBilling ? "yearly" : "monthly"
|
||||
);
|
||||
|
||||
const price = pricingPlans?.getPrice(
|
||||
pricingPlans.isGithubRelease && WebPlan
|
||||
? WebPlan
|
||||
: (product as RNIap.Subscription),
|
||||
pricingPlans.hasTrialOffer(plan.id, product?.productId) ? 1 : 0,
|
||||
annualBilling
|
||||
);
|
||||
|
||||
const isSubscribed =
|
||||
product?.productId &&
|
||||
pricingPlans?.user?.subscription?.productId?.includes(plan.id) &&
|
||||
pricingPlans.isSubscribed();
|
||||
|
||||
const isNotReady =
|
||||
pricingPlans?.loadingPlans || (!price && !WebPlan?.price?.gross);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...getElevationStyle(3),
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: Radius.LG,
|
||||
padding: 16,
|
||||
flexShrink: isTablet ? 1 : undefined,
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
gap: 6
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.xl}>{plan.name} </Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{isSubscribed ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: defaultBorderRadius,
|
||||
padding: Spacing.LEVEL_0,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignSelf: "flex-start",
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.static.white} size={AppFontSize.xxs}>
|
||||
{strings.currentPlan()}
|
||||
</Heading>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{regionalDiscount?.discount || WebPlan?.discount ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.static.red,
|
||||
borderRadius: 100,
|
||||
padding: Spacing.LEVEL_0,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
justifyContent: "center",
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.static.white} size={AppFontSize.xxs}>
|
||||
{strings.percentOff(
|
||||
`${regionalDiscount?.discount || WebPlan?.discount?.amount}`
|
||||
)}
|
||||
</Heading>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{plan.recommended ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.static.red,
|
||||
borderRadius: 100,
|
||||
padding: Spacing.LEVEL_0,
|
||||
paddingHorizontal: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: colors.static.white,
|
||||
fontSize: AppFontSize.xxs
|
||||
}}
|
||||
>
|
||||
{strings.recommended()}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Paragraph>{plan.description}</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{pricingPlans?.loadingPlans || (!price && !WebPlan?.price?.gross) ? (
|
||||
<ActivityIndicator size="small" color={colors.primary.accent} />
|
||||
) : (
|
||||
<View>
|
||||
<Heading size={24}>
|
||||
{price ||
|
||||
`${WebPlan?.price?.currency} ${WebPlan?.price?.gross}`}{" "}
|
||||
<Paragraph color={colors.secondary.heading} fontSize="SM">
|
||||
/{strings.month()}
|
||||
</Paragraph>
|
||||
</Heading>
|
||||
|
||||
{!product && !WebPlan ? null : (
|
||||
<Paragraph
|
||||
color={colors.primary.paragraph}
|
||||
size={AppFontSize.xs}
|
||||
>
|
||||
{annualBilling
|
||||
? strings.billedAnnually(
|
||||
pricingPlans?.getStandardPrice(
|
||||
(product || WebPlan) as any
|
||||
) as string
|
||||
)
|
||||
: strings.billedMonthly(
|
||||
pricingPlans?.getStandardPrice(
|
||||
(product || WebPlan) as any
|
||||
) as string
|
||||
)}
|
||||
</Paragraph>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginVertical: Spacing.LEVEL_4,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{Object.keys(PlanOverView[plan.id as keyof typeof PlanOverView]).map(
|
||||
(item) => (
|
||||
<View
|
||||
key={item + plan.id}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
borderBottomColor: colors.primary.border,
|
||||
borderBottomWidth: 1,
|
||||
paddingBottom: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name={
|
||||
item === "storage"
|
||||
? "cloud"
|
||||
: item === "fileSize"
|
||||
? "file"
|
||||
: "image-outline"
|
||||
}
|
||||
iconFamily="notesnook"
|
||||
size={AppFontSize.lg}
|
||||
/>
|
||||
<Paragraph size={AppFontSize.sm}>
|
||||
{strings[item as "storage" | "hdImages" | "fileSize"]()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Heading size={AppFontSize.sm}>
|
||||
{
|
||||
PlanOverView[plan.id as keyof typeof PlanOverView][
|
||||
item as "storage" | "hdImages" | "fileSize"
|
||||
]
|
||||
}
|
||||
</Heading>
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title={strings.selectPlan()}
|
||||
type={plan.id === "pro" ? "accent" : "tertiary"}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (isNotReady) return;
|
||||
const currentPlanSubscribed =
|
||||
PremiumService.get() &&
|
||||
(pricingPlans?.user?.subscription?.productId ===
|
||||
(product as RNIap.Subscription)?.productId ||
|
||||
pricingPlans?.user?.subscription?.productId?.startsWith(
|
||||
(product as RNIap.Subscription)?.productId
|
||||
));
|
||||
pricingPlans?.selectPlan(
|
||||
plan.id,
|
||||
currentPlanSubscribed
|
||||
? `notesnook.${plan.id}.${
|
||||
!(product as RNIap.Subscription)?.productId.includes(
|
||||
"yearly"
|
||||
)
|
||||
? "yearly"
|
||||
: "monthly"
|
||||
}`
|
||||
: pricingPlans.isGithubRelease
|
||||
? (WebPlan?.period as string)
|
||||
: (product?.productId as string)
|
||||
);
|
||||
setStep(Steps.buy);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
84
apps/mobile/app/components/paywall/review-item.tsx
Normal file
84
apps/mobile/app/components/paywall/review-item.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Image, TouchableOpacity, View } from "react-native";
|
||||
//@ts-ignore
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import Heading from "../ui/typography/heading";
|
||||
|
||||
export const ReviewItem = (props: {
|
||||
review: string;
|
||||
user: string;
|
||||
userSource: string;
|
||||
link: string;
|
||||
userImage?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
onPress={() => {
|
||||
openLinkInBrowser(props.link);
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: Spacing.LEVEL_2,
|
||||
borderWidth: 1,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: Radius.S,
|
||||
borderColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1,
|
||||
paddingBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{props.userImage ? (
|
||||
<Image
|
||||
source={{
|
||||
uri: props.userImage
|
||||
}}
|
||||
style={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<View>
|
||||
<Heading size={AppFontSize.sm}>{props.user}</Heading>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.primary.paragraph}>
|
||||
{props.userSource}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Paragraph>{props.review}</Paragraph>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -43,6 +43,8 @@ import ColorPicker from "../dialogs/color-picker";
|
||||
import PaywallSheet from "../sheets/paywall";
|
||||
import { Button } from "../ui/button";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
|
||||
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -80,12 +82,11 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
|
||||
key={item.id}
|
||||
onPress={toggleColor}
|
||||
style={{
|
||||
width: 35,
|
||||
height: 35,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 5
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{isLinked ? (
|
||||
@@ -151,31 +152,10 @@ export const ColorTags = ({ item }: { item: Note }) => {
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: isTablet ? undefined : 1,
|
||||
flexDirection: "row",
|
||||
marginLeft: 5,
|
||||
flexShrink: 2
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
{!colorNotes || !colorNotes.length ? (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
buttonType={{
|
||||
text: colors.primary.accent
|
||||
}}
|
||||
title={strings.addColor()}
|
||||
type="secondary"
|
||||
icon="plus"
|
||||
iconPosition="right"
|
||||
height={30}
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
{colorNotes?.length ? (
|
||||
<LegendList
|
||||
data={colorNotes}
|
||||
estimatedItemSize={30}
|
||||
@@ -184,11 +164,14 @@ export const ColorTags = ({ item }: { item: Note }) => {
|
||||
bounces={false}
|
||||
renderItem={renderItem}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
ListFooterComponent={
|
||||
<Pressable
|
||||
style={{
|
||||
width: 35,
|
||||
height: 35,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
@@ -197,16 +180,17 @@ export const ColorTags = ({ item }: { item: Note }) => {
|
||||
type="secondary"
|
||||
onPress={onPress}
|
||||
>
|
||||
<Icon
|
||||
<AppIcon
|
||||
testID="icon-plus"
|
||||
iconFamily="notesnook"
|
||||
name="plus"
|
||||
color={colors.primary.icon}
|
||||
color={colors.primary.accent}
|
||||
size={AppFontSize.lg}
|
||||
/>
|
||||
</Pressable>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ import DateTimePickerModal from "react-native-modal-datetime-picker";
|
||||
import { db } from "../../common/database";
|
||||
import { Item, Note } from "@notesnook/core";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
export const DateMeta = ({ item }: { item: Item }) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const [isDatePickerVisible, setIsDatePickerVisible] = useState(false);
|
||||
@@ -50,44 +51,52 @@ export const DateMeta = ({ item }: { item: Item }) => {
|
||||
key={key}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "48.5%",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: Radius.XS,
|
||||
padding: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_1,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{strings.dateDescFromKey(
|
||||
key as
|
||||
| "dateDeleted"
|
||||
| "dateEdited"
|
||||
| "dateModified"
|
||||
| "dateCreated"
|
||||
| "dateUploaded"
|
||||
)}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
onPress={
|
||||
item.type !== "note"
|
||||
? undefined
|
||||
: () => {
|
||||
setIsDatePickerVisible(true);
|
||||
}
|
||||
}
|
||||
>
|
||||
{getFormattedDate(
|
||||
key === "dateCreated"
|
||||
? dateCreated
|
||||
: (item[key as keyof Item] as string),
|
||||
"date-time"
|
||||
)}
|
||||
{key === "dateCreated" && item.type === "note" ? (
|
||||
<>
|
||||
{" "}
|
||||
<AppIcon name="pencil" size={AppFontSize.md} />
|
||||
</>
|
||||
) : null}
|
||||
</Paragraph>
|
||||
<View>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{strings.dateDescFromKey(
|
||||
key as
|
||||
| "dateDeleted"
|
||||
| "dateEdited"
|
||||
| "dateModified"
|
||||
| "dateCreated"
|
||||
| "dateUploaded"
|
||||
)}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.primary.paragraph}
|
||||
fontFamily="MEDIUM"
|
||||
onPress={
|
||||
item.type !== "note"
|
||||
? undefined
|
||||
: () => {
|
||||
setIsDatePickerVisible(true);
|
||||
}
|
||||
}
|
||||
>
|
||||
{getFormattedDate(
|
||||
key === "dateCreated"
|
||||
? dateCreated
|
||||
: (item[key as keyof Item] as string),
|
||||
"date-time"
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{key === "dateCreated" && item.type === "note" ? (
|
||||
<>
|
||||
<AppIcon name="edit-pencil" size={10} iconFamily="notesnook" />
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -119,9 +128,10 @@ export const DateMeta = ({ item }: { item: Item }) => {
|
||||
<View
|
||||
style={{
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.primary.border,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
borderColor: colors.primary.border,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{getDateMeta().map(renderItem)}
|
||||
|
||||
@@ -18,14 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { eOnLoadNote } from "../../utils/events";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
sendItemUpdateEvent,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import { eOnLoadNote, refreshNotesPage } from "../../utils/events";
|
||||
import { fluidTabsRef } from "../../utils/global-refs";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
@@ -37,27 +41,43 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { DateMeta } from "./date-meta";
|
||||
import { Items } from "./items";
|
||||
import Notebooks from "./notebooks";
|
||||
import { TagStrip, Tags } from "./tags";
|
||||
import { Tags } from "./tags";
|
||||
import { Dialog } from "../dialog";
|
||||
|
||||
const Line = ({ top = 6, bottom = 6 }) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: colors.primary.border,
|
||||
width: "100%",
|
||||
marginTop: top,
|
||||
marginBottom: bottom
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { Button } from "../ui/button";
|
||||
import ManageTags from "../../screens/manage-tags";
|
||||
import ColorPicker from "../dialogs/color-picker";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import { useMenuStore } from "../../stores/use-menu-store";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useIsFeatureAvailable } from "@notesnook/common";
|
||||
import PaywallSheet from "../sheets/paywall";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const colorFeature = useIsFeatureAvailable("colors");
|
||||
const [noteNotebooks, setNoteNotebooks] = useState([]);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const colorNotes = useMenuStore((state) => state.colorNotes);
|
||||
useEffect(() => {
|
||||
async function getNotebooks() {
|
||||
let filteredNotebooks = await db.relations.to(item, "notebook").resolve();
|
||||
return filteredNotebooks || [];
|
||||
}
|
||||
if (item.type === "note") {
|
||||
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
|
||||
db.relations
|
||||
.to(item, "tag")
|
||||
.resolve()
|
||||
.then((tags) => {
|
||||
setTags(tags);
|
||||
});
|
||||
}
|
||||
}, [item]);
|
||||
|
||||
if (!item || !item.id) {
|
||||
return (
|
||||
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
|
||||
@@ -74,7 +94,8 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
backgroundColor: colors.primary.background,
|
||||
borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1,
|
||||
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
|
||||
maxHeight: "100%"
|
||||
maxHeight: "100%",
|
||||
paddingTop: Spacing.LEVEL_3
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
bounces={false}
|
||||
@@ -83,111 +104,240 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
renderItem={() => (
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{item.type === "note" ? (
|
||||
<ColorPicker
|
||||
visible={visible}
|
||||
setVisible={setVisible}
|
||||
onColorAdded={async (color) => {
|
||||
await db.relations.to(item, "color").unlink();
|
||||
await db.relations.add(color, item);
|
||||
useRelationStore.getState().update();
|
||||
useMenuStore.getState().setColorNotes();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
sendItemUpdateEvent(color.id, "color");
|
||||
eSendEvent(refreshNotesPage);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexShrink: 1,
|
||||
gap: 5
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{item.type === "color" ? (
|
||||
<Pressable
|
||||
type="accent"
|
||||
accentColor={item.colorCode}
|
||||
accentText={colors.static.white}
|
||||
{noteNotebooks?.map((item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 100,
|
||||
marginRight: 10
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
paddingVertical: 2,
|
||||
backgroundColor: colors.secondary.background,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
/>
|
||||
) : item.type === "tag" ? (
|
||||
<Icon
|
||||
name="pound"
|
||||
size={AppFontSize.lg}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
) : null}
|
||||
>
|
||||
<AppIcon
|
||||
name="bookmark"
|
||||
iconFamily="notesnook"
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<Heading size={AppFontSize.lg}>{item.title}</Heading>
|
||||
{tags?.map((item) =>
|
||||
item.id ? (
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
paddingVertical: 2,
|
||||
backgroundColor: colors.secondary.background,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null
|
||||
)}
|
||||
</View>
|
||||
|
||||
{item.type === "note" ? (
|
||||
<IconButton
|
||||
name="open-in-new"
|
||||
type="plain"
|
||||
color={colors.primary.icon}
|
||||
size={AppFontSize.lg}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-start"
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexShrink: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
onPress={() => {
|
||||
close();
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: item,
|
||||
newTab: true
|
||||
});
|
||||
if (!DDS.isTab) {
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
}
|
||||
>
|
||||
{item.type === "color" ? (
|
||||
<Pressable
|
||||
type="accent"
|
||||
accentColor={item.colorCode}
|
||||
accentText={colors.static.white}
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
) : item.type === "tag" ? (
|
||||
<AppIcon
|
||||
name="shopping-mode"
|
||||
iconFamily="evilicons"
|
||||
size={AppFontSize.lg}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Heading size={AppFontSize.xl}>{item.title}</Heading>
|
||||
</View>
|
||||
|
||||
{item.type === "note" ? (
|
||||
<IconButton
|
||||
name="square-out"
|
||||
iconFamily="notesnook"
|
||||
type="plain"
|
||||
color={colors.primary.icon}
|
||||
size={AppFontSize.lg}
|
||||
style={{
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
onPress={() => {
|
||||
close();
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: item,
|
||||
newTab: true
|
||||
});
|
||||
if (!DDS.isTab) {
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{(item.type === "notebook" || item.type === "reminder") &&
|
||||
item.description ? (
|
||||
<Paragraph>{item.description}</Paragraph>
|
||||
) : null}
|
||||
|
||||
{item.type === "reminder" ? (
|
||||
<ReminderTime
|
||||
reminder={item}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
alignSelf: "flex-start",
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{(item.type === "notebook" || item.type === "reminder") &&
|
||||
item.description ? (
|
||||
<Paragraph>{item.description}</Paragraph>
|
||||
<DateMeta item={item} />
|
||||
|
||||
{item.type === "note" && colorNotes.length > 0 ? (
|
||||
<Tags close={close} item={item} />
|
||||
) : null}
|
||||
|
||||
{item.type === "note" ? (
|
||||
<TagStrip close={close} item={item} />
|
||||
) : null}
|
||||
|
||||
{item.type === "reminder" ? (
|
||||
<ReminderTime
|
||||
reminder={item}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
alignSelf: "flex-start",
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 1,
|
||||
borderTopWidth: colorNotes.length > 0 ? 1 : 0,
|
||||
borderColor: colors.primary.border,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
paddingTop: colorNotes.length > 0 ? Spacing.LEVEL_2 : 0,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
ManageTags.present([item.id]);
|
||||
close();
|
||||
}}
|
||||
buttonType={{
|
||||
text: colors.primary.paragraph
|
||||
}}
|
||||
title={strings.addTag()}
|
||||
type={colorNotes?.length ? "accent-outline" : "shade"}
|
||||
icon="plus"
|
||||
iconFamily="notesnook"
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
width: colorNotes?.length > 0 ? "100%" : "48.5%"
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{colorNotes.length > 0 ? null : (
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (colorFeature && !colorFeature.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: colorFeature.error,
|
||||
type: "info",
|
||||
context: "local",
|
||||
actionText: strings.upgrade(),
|
||||
func: () => {
|
||||
PaywallSheet.present(colorFeature);
|
||||
ToastManager.hide();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
useSettingStore.getState().setSheetKeyboardHandler(false);
|
||||
setVisible(true);
|
||||
}}
|
||||
title={strings.addColor()}
|
||||
type="secondaryAccented"
|
||||
icon="plus"
|
||||
iconFamily="notesnook"
|
||||
style={{
|
||||
width: "48.5%",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: Spacing.LEVEL_2
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<DateMeta item={item} />
|
||||
<Line bottom={0} top={0} />
|
||||
|
||||
{item.type === "note" ? (
|
||||
<>
|
||||
<Tags close={close} item={item} />
|
||||
<Line bottom={0} top={0} />
|
||||
</>
|
||||
) : null}
|
||||
{item.type === "note" ? (
|
||||
{/* {item.type === "note" ? (
|
||||
<Notebooks note={item} close={close} />
|
||||
) : null}
|
||||
) : null} */}
|
||||
<Items
|
||||
item={item}
|
||||
buttons={buttons}
|
||||
|
||||
@@ -27,12 +27,13 @@ import { Action, ActionId, useActions } from "../../hooks/use-actions";
|
||||
import { useStoredRef } from "../../hooks/use-stored-ref";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
|
||||
const TOP_BAR_ITEMS: ActionId[] = [
|
||||
"pin",
|
||||
@@ -152,7 +153,7 @@ export const Items = ({
|
||||
key={item.id}
|
||||
style={{
|
||||
alignItems: "center",
|
||||
width: columnItemWidth - 8,
|
||||
width: columnItemWidth - 10,
|
||||
opacity: item.locked ? 0.5 : 1
|
||||
}}
|
||||
>
|
||||
@@ -161,12 +162,12 @@ export const Items = ({
|
||||
type={item.checked ? "shade" : "secondary"}
|
||||
testID={"icon-" + item.id}
|
||||
style={{
|
||||
height: columnItemWidth / 1.5,
|
||||
width: columnItemWidth - 8,
|
||||
width: columnItemWidth - 10,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
borderRadius: 10,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom: DDS.isTab ? 7 : 3.5
|
||||
marginBottom: 6
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
@@ -253,7 +254,7 @@ export const Items = ({
|
||||
key={item.id}
|
||||
testID={"icon-" + item.id}
|
||||
style={{
|
||||
width: columnItemWidth - 8,
|
||||
width: columnItemWidth - 10,
|
||||
alignSelf: "flex-start",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
@@ -261,24 +262,25 @@ export const Items = ({
|
||||
<View
|
||||
style={{
|
||||
height: columnItemWidth / 2,
|
||||
width: columnItemWidth - DefaultAppStyles.GAP_SMALL,
|
||||
width: 65,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderColor: item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
: colors.primary.border,
|
||||
overflow: "hidden"
|
||||
backgroundColor: item.checked
|
||||
? colors.primary.shade
|
||||
: colors.secondary.background,
|
||||
borderRadius: Radius.XS,
|
||||
overflow: "hidden",
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
paddingHorizontal: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
allowFontScaling
|
||||
size={DDS.isTab ? AppFontSize.xxl : AppFontSize.md + 4}
|
||||
size={16}
|
||||
color={
|
||||
item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
? colors.primary.icon
|
||||
: item.id === "delete" || item.id === "trash"
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
@@ -310,7 +312,13 @@ export const Items = ({
|
||||
|
||||
<Paragraph
|
||||
textBreakStrategy="simple"
|
||||
size={AppFontSize.xxs}
|
||||
fontSize="XXS"
|
||||
fontFamily="MEDIUM"
|
||||
color={
|
||||
item.checked
|
||||
? colors.primary.paragraph
|
||||
: colors.secondary.paragraph
|
||||
}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{item.title}
|
||||
@@ -321,8 +329,12 @@ export const Items = ({
|
||||
[
|
||||
colors.error.icon,
|
||||
colors.primary.accent,
|
||||
colors.primary.border,
|
||||
colors.primary.icon,
|
||||
colors.primary.paragraph,
|
||||
colors.primary.shade,
|
||||
colors.secondary.background,
|
||||
colors.secondary.icon,
|
||||
colors.secondary.paragraph,
|
||||
colors.static.orange,
|
||||
columnItemWidth,
|
||||
topBarSorting
|
||||
@@ -352,16 +364,18 @@ export const Items = ({
|
||||
autoplay={false}
|
||||
showPagination
|
||||
paginationStyleItemActive={{
|
||||
borderRadius: 2,
|
||||
backgroundColor: colors.selected.background,
|
||||
height: 6,
|
||||
marginHorizontal: 2
|
||||
borderRadius: 6,
|
||||
backgroundColor: colors.selected.accent,
|
||||
height: 5,
|
||||
width: 20,
|
||||
marginHorizontal: 3
|
||||
}}
|
||||
paginationStyleItemInactive={{
|
||||
borderRadius: 2,
|
||||
borderRadius: 6,
|
||||
backgroundColor: colors.secondary.background,
|
||||
height: 6,
|
||||
marginHorizontal: 2
|
||||
height: 5,
|
||||
width: 14,
|
||||
marginHorizontal: 3
|
||||
}}
|
||||
paginationStyle={{
|
||||
position: "relative",
|
||||
@@ -380,7 +394,7 @@ export const Items = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: 5,
|
||||
gap: Spacing.LEVEL_2,
|
||||
width: width
|
||||
}}
|
||||
>
|
||||
@@ -394,7 +408,7 @@ export const Items = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 5,
|
||||
gap: Spacing.LEVEL_1,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -17,18 +17,17 @@ 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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import ManageTags from "../../screens/manage-tags";
|
||||
import { TaggedNotes } from "../../screens/notes/tagged";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import { ColorTags } from "./color-tags";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
|
||||
export const Tags = ({ item, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -37,32 +36,12 @@ export const Tags = ({ item, close }) => {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
alignSelf: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
borderTopWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
paddingVertical: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
ManageTags.present([item.id]);
|
||||
close();
|
||||
}}
|
||||
buttonType={{
|
||||
text: colors.primary.accent
|
||||
}}
|
||||
title={strings.addTag()}
|
||||
type="secondary"
|
||||
icon="plus"
|
||||
iconPosition="right"
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
/>
|
||||
<ColorTags item={item} />
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
@@ -34,6 +34,8 @@ import SheetWrapper from "../ui/sheet";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
const SheetProvider = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -113,43 +115,78 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
: data.enableGesturesInScrollView
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom:
|
||||
!data.progress && !data.icon && !data.title && !data.paragraph
|
||||
? 0
|
||||
: 10,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
{data?.progress ? (
|
||||
<ActivityIndicator
|
||||
{data?.progress || data?.icon || data?.paragraph ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom:
|
||||
!data.progress && !data.icon && !data.title && !data.paragraph
|
||||
? 0
|
||||
: 10,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1,
|
||||
paddingTop: Spacing.LEVEL_2
|
||||
}}
|
||||
size={50}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
) : null}
|
||||
>
|
||||
{data?.icon || data?.progress ? (
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
{data.icon ? (
|
||||
<AppIcon
|
||||
name="clock"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
) : (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP
|
||||
}}
|
||||
size={50}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{data?.icon ? (
|
||||
<Icon
|
||||
color={colors[data.iconColor] || colors.primary.accent}
|
||||
name={data.icon}
|
||||
size={50}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{data?.title ? <Heading> {data?.title}</Heading> : null}
|
||||
|
||||
{data?.paragraph ? (
|
||||
<Paragraph style={{ textAlign: "center" }}>
|
||||
{data?.paragraph}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
{data?.title || data?.paragraph ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{data.title ? (
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{data.title}
|
||||
</Heading>
|
||||
) : null}
|
||||
{data.paragraph ? (
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{data.paragraph}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{typeof data.component === "function"
|
||||
? data.component(
|
||||
@@ -212,20 +249,27 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{data?.actionsArray &&
|
||||
data?.actionsArray.map((item) => (
|
||||
<Button
|
||||
onPress={item.action}
|
||||
key={item.accentText}
|
||||
title={item.actionText}
|
||||
icon={item.icon && item.icon}
|
||||
type={item.type || "accent"}
|
||||
style={{
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
width="100%"
|
||||
/>
|
||||
))}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{data?.actionsArray &&
|
||||
data?.actionsArray.map((item) => (
|
||||
<Button
|
||||
onPress={item.action}
|
||||
key={item.accentText}
|
||||
title={item.actionText}
|
||||
icon={item.icon && item.icon}
|
||||
type={item.type || "accent"}
|
||||
style={{
|
||||
flex: 1,
|
||||
flexShrink: 1
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{data?.learnMore ? (
|
||||
<Paragraph
|
||||
|
||||
@@ -16,23 +16,24 @@ 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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { db } from "../../../common/database";
|
||||
import { ToastManager, presentSheet } from "../../../services/event-manager";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Button } from "../../ui/button";
|
||||
import FormInput, {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../ui/input/form-input";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { ToastManager, presentSheet } from "../../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
const getExpiryOptions = () => [
|
||||
{ label: strings.expiryOneDay(), value: 24 * 60 * 60 * 1000 },
|
||||
@@ -94,7 +95,7 @@ export default function AddApiKeySheet({ close, onAdd }: AddApiKeySheetProps) {
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
gap: DefaultAppStyles.GAP,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL * 2
|
||||
}}
|
||||
@@ -102,60 +103,92 @@ export default function AddApiKeySheet({ close, onAdd }: AddApiKeySheetProps) {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
alignItems: "flex-start",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.xl}>{strings.createApiKey()}</Heading>
|
||||
</View>
|
||||
|
||||
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.keyName()}</Paragraph>
|
||||
<FormInput
|
||||
name="keyName"
|
||||
formRef={formRef}
|
||||
placeholder={strings.exampleKeyName()}
|
||||
validators={[validators.required(strings.enterKeyName())]}
|
||||
onChangeText={() => {
|
||||
formRef.current.setError("keyName", undefined);
|
||||
}}
|
||||
onSubmitEditing={handleCreate}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.expiresIn()}</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
{getExpiryOptions().map((option) => (
|
||||
<Pressable
|
||||
key={option.label}
|
||||
onPress={() => setSelectedExpiry(option.value)}
|
||||
type={
|
||||
selectedExpiry === option.value ? "selected" : "transparent"
|
||||
}
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
color={
|
||||
selectedExpiry === option.value
|
||||
? colors.selected.paragraph
|
||||
: colors.primary.paragraph
|
||||
}
|
||||
<AppIcon
|
||||
name="key"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1, gap: DefaultAppStyles.GAP_VERTICAL_SMALL }}>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.createApiKey()}
|
||||
</Heading>
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{strings.createApiKeyDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<FormInput
|
||||
name="keyName"
|
||||
formRef={formRef}
|
||||
label={strings.keyName()}
|
||||
placeholder={strings.exampleKeyName()}
|
||||
validators={[validators.required(strings.enterKeyName())]}
|
||||
containerStyle={{ borderRadius: Radius.XS }}
|
||||
onChangeText={() => {
|
||||
formRef.current.setError("keyName", undefined);
|
||||
}}
|
||||
onSubmitEditing={handleCreate}
|
||||
/>
|
||||
|
||||
<View style={{ height: 1, backgroundColor: colors.primary.border }} />
|
||||
|
||||
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
|
||||
<Heading fontSize="MD" lineHeight="100%">
|
||||
{strings.expiresIn()}
|
||||
</Heading>
|
||||
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
|
||||
{getExpiryOptions().map((option) => {
|
||||
const selected = selectedExpiry === option.value;
|
||||
return (
|
||||
<Pressable
|
||||
key={option.label}
|
||||
onPress={() => setSelectedExpiry(option.value)}
|
||||
type={selected ? "selected" : "transparent"}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: Spacing.LEVEL_2,
|
||||
borderRadius: Radius.XS,
|
||||
borderWidth: selected ? 0 : 1,
|
||||
borderColor: colors.secondary.border
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
))}
|
||||
<Heading
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="SM"
|
||||
lineHeight="100%"
|
||||
color={
|
||||
selected ? colors.selected.heading : colors.secondary.heading
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</Heading>
|
||||
<AppIcon
|
||||
name={selected ? "radiobox-marked" : "radiobox-blank"}
|
||||
size={16}
|
||||
color={selected ? colors.selected.accent : colors.secondary.icon}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -166,9 +199,6 @@ export default function AddApiKeySheet({ close, onAdd }: AddApiKeySheetProps) {
|
||||
loading={isCreating}
|
||||
disabled={isCreating}
|
||||
onPress={handleCreate}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
@@ -31,15 +31,18 @@ import { useNotebookStore } from "../../../stores/use-notebook-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { eOnNotebookUpdated } from "../../../utils/events";
|
||||
import { getParentNotebookId } from "../../../utils/notebooks";
|
||||
import Input from "../../ui/input";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { defaultBorderRadius } from "../../../utils/size";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getContainerBorder } from "../../../utils/colors";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import FormInput, {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../ui/input/form-input";
|
||||
|
||||
export const AddNotebookSheet = ({
|
||||
notebook,
|
||||
@@ -55,29 +58,28 @@ export const AddNotebookSheet = ({
|
||||
defaultTitle?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const title = useRef(notebook?.title || defaultTitle);
|
||||
const description = useRef(notebook?.description);
|
||||
const titleInput = useRef<TextInput>(null);
|
||||
const descriptionInput = useRef<TextInput>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
title: notebook?.title || defaultTitle || "",
|
||||
description: notebook?.description || ""
|
||||
})
|
||||
);
|
||||
|
||||
const onSaveChanges = async () => {
|
||||
if (loading) return;
|
||||
|
||||
if (!title.current || title?.current.trim().length === 0) {
|
||||
ToastManager.show({
|
||||
heading: strings.enterNotebookTitle(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!formRef.current?.validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const values = formRef.current.getValues();
|
||||
|
||||
const id = await db.notebooks.add({
|
||||
title: title.current,
|
||||
description: description.current,
|
||||
title: values.title,
|
||||
description: values.description,
|
||||
id: notebook?.id
|
||||
});
|
||||
|
||||
@@ -127,57 +129,67 @@ export const AddNotebookSheet = ({
|
||||
...getElevationStyle(5),
|
||||
width: DDS.isTab ? 400 : "85%",
|
||||
maxHeight: 450,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: Radius.LG,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12,
|
||||
gap: Spacing.LEVEL_4,
|
||||
paddingVertical: Spacing.LEVEL_4,
|
||||
...getContainerBorder(colors.primary.border, 0.5),
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
gap: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={notebook ? strings.editNotebook() : strings.newNotebook()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={titleInput}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.title}
|
||||
onChangeText={(value) => {
|
||||
title.current = value;
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
onLayout={() => {
|
||||
setTimeout(() => {
|
||||
titleInput?.current?.focus();
|
||||
}, 300);
|
||||
}}
|
||||
placeholder={strings.enterNotebookTitle()}
|
||||
onSubmit={() => {
|
||||
descriptionInput.current?.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.title : title.current}
|
||||
/>
|
||||
>
|
||||
<FormInput
|
||||
fwdRef={titleInput}
|
||||
name="title"
|
||||
formRef={formRef}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.title}
|
||||
onLayout={() => {
|
||||
setTimeout(() => {
|
||||
titleInput?.current?.focus();
|
||||
}, 300);
|
||||
}}
|
||||
validators={[validators.required(strings.titleIsRequired())]}
|
||||
placeholder={"eg. My Notebook"}
|
||||
onSubmitEditing={() => {
|
||||
descriptionInput.current?.focus();
|
||||
}}
|
||||
label={strings.enterNotebookTitle()}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={
|
||||
notebook ? notebook.title : formRef.current.getValue("title")
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={descriptionInput}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.description}
|
||||
onChangeText={(value) => {
|
||||
description.current = value;
|
||||
}}
|
||||
placeholder={strings.enterNotebookDescription()}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : ""}
|
||||
/>
|
||||
<FormInput
|
||||
fwdRef={descriptionInput}
|
||||
name="description"
|
||||
formRef={formRef}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.description}
|
||||
label={strings.enterNotebookDescription()}
|
||||
placeholder={"eg. This is My Notebook"}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : ""}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<DialogButtons
|
||||
onPressNegative={() => {
|
||||
title.current = undefined;
|
||||
close?.(false);
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
}}
|
||||
|
||||
178
apps/mobile/app/components/sheets/app-lock-timeout/index.tsx
Normal file
178
apps/mobile/app/components/sheets/app-lock-timeout/index.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { useWindowDimensions, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
const OPTIONS = [-1, 0, 1, 5, 15, 30];
|
||||
|
||||
const formatValue = (item: number) => {
|
||||
return item === -1
|
||||
? strings.never()
|
||||
: item === 0
|
||||
? strings.immediately()
|
||||
: item === 1
|
||||
? strings.minutes(1)
|
||||
: strings.minutes(item);
|
||||
};
|
||||
|
||||
type AppLockTimeoutProps = {
|
||||
close?: (ctx?: string) => void;
|
||||
};
|
||||
|
||||
function AppLockTimeout({ close }: AppLockTimeoutProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useWindowDimensions();
|
||||
const [currentValue, setCurrentValue] = useState(
|
||||
useSettingStore.getState().settings.appLockTimer
|
||||
);
|
||||
|
||||
const onChange = async (item: number) => {
|
||||
SettingsService.set({ appLockTimer: item });
|
||||
setCurrentValue(item);
|
||||
close?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{
|
||||
maxHeight: height * 0.8
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1,
|
||||
paddingTop: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="clock"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.appLockTimeout()}
|
||||
</Heading>
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{strings.appLockTimeoutDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopLeftRadius: 35,
|
||||
borderTopRightRadius: 35,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{OPTIONS.map((item) => {
|
||||
const selected = currentValue === item;
|
||||
return (
|
||||
<Pressable
|
||||
key={item}
|
||||
type={selected ? "selected" : "transparent"}
|
||||
onPress={() => onChange(item)}
|
||||
style={{
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: Spacing.LEVEL_2,
|
||||
borderRadius: Radius.XS,
|
||||
borderWidth: selected ? 0 : 1,
|
||||
borderColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="SM"
|
||||
lineHeight="100%"
|
||||
color={colors.primary.heading}
|
||||
>
|
||||
{formatValue(item)}
|
||||
</Heading>
|
||||
|
||||
<AppIcon
|
||||
name={selected ? "radio-button" : "ellipse"}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={
|
||||
selected
|
||||
? [colors.selected.accent, colors.static.white]
|
||||
: colors.secondary.icon
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
AppLockTimeout.present = () => {
|
||||
presentSheet({
|
||||
component: (_ref, close) => <AppLockTimeout close={close} />
|
||||
});
|
||||
};
|
||||
|
||||
export default AppLockTimeout;
|
||||
@@ -23,7 +23,6 @@ import dayjs from "dayjs";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Linking,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
@@ -41,12 +40,15 @@ import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { Button } from "../../ui/button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
const isGithubRelease = Config.GITHUB_RELEASE === "true";
|
||||
export const BuyPlan = (props: {
|
||||
planId: string;
|
||||
canActivateTrial?: boolean;
|
||||
pricingPlans: ReturnType<typeof usePricingPlans>;
|
||||
}) => {
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const { colors } = useThemeColors();
|
||||
const [checkoutUrl, setCheckoutUrl] = useState<string>();
|
||||
const pricingPlans = props.pricingPlans;
|
||||
@@ -63,6 +65,18 @@ export const BuyPlan = (props: {
|
||||
: (pricingPlans.selectedProduct as RNIap.Product)?.productId
|
||||
)?.includes("5");
|
||||
|
||||
const isAnnual = isGithubRelease
|
||||
? (pricingPlans.selectedProduct as Plan)?.period === "yearly"
|
||||
: (pricingPlans.selectedProduct as RNIap.Product)?.productId?.includes(
|
||||
"yearly"
|
||||
);
|
||||
|
||||
const hasTrialOffer = pricingPlans.hasTrialOffer(
|
||||
props.planId,
|
||||
(pricingPlans.selectedProduct as RNIap.Product)?.productId ||
|
||||
(pricingPlans.selectedProduct as Plan)?.period
|
||||
);
|
||||
|
||||
return checkoutUrl ? (
|
||||
<View
|
||||
style={{
|
||||
@@ -88,162 +102,278 @@ export const BuyPlan = (props: {
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
<View
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingVertical: Spacing.LEVEL_4
|
||||
}}
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
>
|
||||
{[
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
? "yearly"
|
||||
: `notesnook.${props.planId}.yearly`,
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
? "monthly"
|
||||
: `notesnook.${props.planId}.monthly`,
|
||||
...(props.planId === "essential" || pricingPlans.isSubscribed()
|
||||
? []
|
||||
: [
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
? "5-year"
|
||||
: `notesnook.${props.planId}.5year`
|
||||
])
|
||||
].map((item) => (
|
||||
<ProductItem
|
||||
key={item}
|
||||
pricingPlans={pricingPlans}
|
||||
productId={item}
|
||||
/>
|
||||
))}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.primary.paragraph} size={AppFontSize.sm}>
|
||||
{strings.dueToday()}{" "}
|
||||
{pricingPlans.hasTrialOffer(
|
||||
props.planId,
|
||||
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
|
||||
(pricingPlans?.selectedProduct as Plan)?.period
|
||||
) ? (
|
||||
<Text
|
||||
style={{
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
({strings.daysFree(`${billingDuration?.duration || 0}`)})
|
||||
</Text>
|
||||
) : null}
|
||||
</Heading>
|
||||
|
||||
<Paragraph color={colors.primary.paragraph}>
|
||||
{pricingPlans.hasTrialOffer(
|
||||
props.planId,
|
||||
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
|
||||
(pricingPlans?.selectedProduct as Plan)?.period
|
||||
)
|
||||
? "FREE"
|
||||
: pricingPlans.getStandardPrice(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{pricingPlans.hasTrialOffer(
|
||||
props.planId,
|
||||
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
|
||||
(pricingPlans?.selectedProduct as Plan)?.period
|
||||
) ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{strings.due(
|
||||
dayjs()
|
||||
.add(billingDuration?.duration || 0, "day")
|
||||
.format("DD MMMM")
|
||||
)}
|
||||
</Paragraph>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{pricingPlans.getStandardPrice(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{pricingPlans.hasTrialOffer(
|
||||
props.planId,
|
||||
(pricingPlans.selectedProduct as RNIap.Product)?.productId ||
|
||||
(pricingPlans.selectedProduct as Plan)?.period
|
||||
) || is5YearPlanSelected ? (
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius
|
||||
}}
|
||||
>
|
||||
{(is5YearPlanSelected
|
||||
? strings["5yearPlanConditions"]()
|
||||
: [
|
||||
strings.trialPlanConditions[0](
|
||||
billingDuration?.duration as number as never
|
||||
),
|
||||
...(isGithubRelease
|
||||
? []
|
||||
: [strings.trialPlanConditions[1](Platform.OS as never)])
|
||||
]
|
||||
).map((item) => (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
flex: 1
|
||||
}}
|
||||
{[
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
? "yearly"
|
||||
: `notesnook.${props.planId}.yearly`,
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
? "monthly"
|
||||
: `notesnook.${props.planId}.monthly`,
|
||||
...(props.planId === "essential" || pricingPlans.isSubscribed()
|
||||
? []
|
||||
: [
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
? "5-year"
|
||||
: `notesnook.${props.planId}.5year`
|
||||
])
|
||||
].map((item) => (
|
||||
<ProductItem
|
||||
key={item}
|
||||
>
|
||||
<Icon
|
||||
color={colors.primary.accent}
|
||||
size={AppFontSize.lg}
|
||||
name="check"
|
||||
/>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Paragraph>
|
||||
</View>
|
||||
pricingPlans={pricingPlans}
|
||||
productId={item}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<Paragraph
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="MD"
|
||||
color={colors.secondary.heading}
|
||||
>
|
||||
{strings.paymentSummary()}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: Radius.S,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.primary.paragraph} size={AppFontSize.md}>
|
||||
{strings.dueToday()}{" "}
|
||||
</Heading>
|
||||
<Paragraph>
|
||||
{hasTrialOffer ? (
|
||||
<Text>
|
||||
{strings.freeTrialIncludes(
|
||||
billingDuration?.duration || 0
|
||||
)}
|
||||
</Text>
|
||||
) : is5YearPlanSelected ? (
|
||||
strings.billingType.annual()
|
||||
) : null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Heading color={colors.primary.accent} fontSize="SM">
|
||||
{hasTrialOffer
|
||||
? "Free"
|
||||
: pricingPlans.getStandardPrice(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription
|
||||
)}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
{hasTrialOffer ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 1,
|
||||
backgroundColor: colors.primary.border,
|
||||
marginVertical: Spacing.LEVEL_2
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
color={colors.primary.paragraph}
|
||||
size={AppFontSize.md}
|
||||
>
|
||||
{strings.nextBillingDate()}
|
||||
</Heading>
|
||||
<Paragraph>
|
||||
{dayjs()
|
||||
.add(billingDuration?.duration || 0, "day")
|
||||
.format("DD MMMM,YYYY")}{" "}
|
||||
•{" "}
|
||||
{isAnnual
|
||||
? strings.billingType.annual()
|
||||
: is5YearPlanSelected
|
||||
? strings.billingType.oneTime()
|
||||
: strings.billingType.monthly()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Heading fontSize="SM" color={colors.primary.accent}>
|
||||
{pricingPlans.getStandardPrice(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription
|
||||
)}
|
||||
</Heading>
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="MD"
|
||||
color={colors.secondary.heading}
|
||||
>
|
||||
{strings.whatsIncluded()}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_2,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
padding: Spacing.LEVEL_3,
|
||||
borderRadius: Radius.S,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 27,
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{[
|
||||
strings.planWhatsIncluded.unlimitedNotes(),
|
||||
strings.planWhatsIncluded.endToEnd(),
|
||||
strings.planWhatsIncluded.allDevices(),
|
||||
hasTrialOffer
|
||||
? strings.planWhatsIncluded.freeTrial(
|
||||
billingDuration?.duration || 0
|
||||
)
|
||||
: undefined,
|
||||
hasTrialOffer ? strings.planWhatsIncluded.remind() : undefined,
|
||||
...(is5YearPlanSelected
|
||||
? strings["5yearPlanConditions"]()
|
||||
: ([] as string[]))
|
||||
].map((item) =>
|
||||
!item ? null : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1,
|
||||
flex: 1
|
||||
}}
|
||||
key={item}
|
||||
>
|
||||
<Icon
|
||||
color={colors.primary.accent}
|
||||
size={AppFontSize.lg}
|
||||
name="check"
|
||||
/>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_0
|
||||
}}
|
||||
fontSize="XS"
|
||||
>
|
||||
<Heading fontSize="XS">{strings.note()}: </Heading>
|
||||
{is5YearPlanSelected
|
||||
? strings.oneTimePurchase()
|
||||
: strings.cancelAnytimeAlt()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xs}
|
||||
>
|
||||
{strings.subTerms[0]()}{" "}
|
||||
<Text
|
||||
style={{
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos");
|
||||
}}
|
||||
>
|
||||
{strings.subTerms[1]()}
|
||||
</Text>{" "}
|
||||
{strings.subTerms[2]()}{" "}
|
||||
<Text
|
||||
style={{
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy");
|
||||
}}
|
||||
>
|
||||
{strings.subTerms[3]()}
|
||||
</Text>
|
||||
</Paragraph>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%",
|
||||
padding: Spacing.LEVEL_3,
|
||||
marginBottom: -insets.bottom,
|
||||
paddingBottom: insets.bottom,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
width="100%"
|
||||
type="accent"
|
||||
@@ -278,50 +408,8 @@ export const BuyPlan = (props: {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xs}
|
||||
>
|
||||
{is5YearPlanSelected
|
||||
? strings.oneTimePurchase()
|
||||
: strings.cancelAnytimeAlt()}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xs}
|
||||
>
|
||||
{strings.subTerms[0]()}{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy");
|
||||
}}
|
||||
>
|
||||
{strings.subTerms[1]()}
|
||||
</Text>{" "}
|
||||
{strings.subTerms[2]()}{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos");
|
||||
}}
|
||||
>
|
||||
{strings.subTerms[3]()}
|
||||
</Text>
|
||||
</Paragraph>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -389,7 +477,13 @@ const ProductItem = (props: {
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 10,
|
||||
opacity: isSubscribed ? 0.5 : 1
|
||||
opacity: isSubscribed ? 0.5 : 1,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: Spacing.LEVEL_2,
|
||||
borderRadius: Radius.S,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
@@ -408,60 +502,85 @@ const ProductItem = (props: {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
|
||||
color={isSelected ? colors.primary.accent : colors.secondary.icon}
|
||||
size={AppFontSize.lg}
|
||||
/>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
|
||||
color={isSelected ? colors.primary.accent : colors.secondary.icon}
|
||||
size={AppFontSize.lg}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.md}>
|
||||
{isAnnual
|
||||
? strings.yearly()
|
||||
: is5YearProduct
|
||||
? strings.fiveYearPlan()
|
||||
: strings.monthly()}
|
||||
</Heading>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.md}>
|
||||
{isAnnual
|
||||
? strings.yearly()
|
||||
: is5YearProduct
|
||||
? strings.fiveYearPlan()
|
||||
: strings.monthly()}
|
||||
</Heading>
|
||||
|
||||
{discountValue ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.static.red,
|
||||
borderRadius: defaultBorderRadius,
|
||||
paddingHorizontal: 6,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.static.white} size={AppFontSize.xs}>
|
||||
{strings.bestValue()} - {strings.percentOff(`${discountValue}`)}
|
||||
</Heading>
|
||||
</View>
|
||||
) : null}
|
||||
{discountValue ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: defaultBorderRadius,
|
||||
padding: Spacing.LEVEL_0,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.static.white} size={AppFontSize.xxs}>
|
||||
{strings.percentOff(`${discountValue}`)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{isSubscribed ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: defaultBorderRadius,
|
||||
paddingHorizontal: 6,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.static.white} size={AppFontSize.xs}>
|
||||
{strings.currentPlan()}
|
||||
</Heading>
|
||||
</View>
|
||||
) : null}
|
||||
{isSubscribed ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: defaultBorderRadius,
|
||||
paddingHorizontal: 6,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.static.white} size={AppFontSize.xs}>
|
||||
{strings.currentPlan()}
|
||||
</Heading>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Paragraph>
|
||||
{is5YearProduct
|
||||
? strings.billingType.oneTime()
|
||||
: isAnnual
|
||||
? strings.billingType.annual()
|
||||
: strings.billingType.monthly()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Paragraph size={AppFontSize.md}>
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.sm}>
|
||||
{isAnnual || is5YearProduct
|
||||
? `${props.pricingPlans.getPrice(
|
||||
product as RNIap.Subscription,
|
||||
@@ -472,14 +591,23 @@ const ProductItem = (props: {
|
||||
? 1
|
||||
: 0,
|
||||
isAnnual
|
||||
)}/${strings.month()}`
|
||||
)}`
|
||||
: null}
|
||||
|
||||
{!isAnnual && !is5YearProduct
|
||||
? `${props.pricingPlans.getStandardPrice(
|
||||
product as RNIap.Subscription
|
||||
)}/${strings.month()}`
|
||||
)}`
|
||||
: null}
|
||||
</Heading>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "flex-end"
|
||||
}}
|
||||
size={AppFontSize.xs}
|
||||
>
|
||||
/month
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
185
apps/mobile/app/components/sheets/date-format/index.tsx
Normal file
185
apps/mobile/app/components/sheets/date-format/index.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
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 { DATE_FORMATS } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import dayjs from "dayjs";
|
||||
import React, { useState } from "react";
|
||||
import { useWindowDimensions, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
type DateFormatProps = {
|
||||
close?: (ctx?: string) => void;
|
||||
};
|
||||
|
||||
function DateFormat({ close }: DateFormatProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useWindowDimensions();
|
||||
const [currentValue, setCurrentValue] = useState(db.settings.getDateFormat());
|
||||
|
||||
const onChange = async (item: string) => {
|
||||
db.settings.setDateFormat(item);
|
||||
useSettingStore.setState({
|
||||
dateFormat: item
|
||||
});
|
||||
setCurrentValue(item);
|
||||
close?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{
|
||||
maxHeight: height * 0.8
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopLeftRadius: 35,
|
||||
borderTopRightRadius: 35,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="calendar-day"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.dateFormat()}
|
||||
</Heading>
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{strings.dateFormatDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: colors.primary.border
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{DATE_FORMATS.map((item) => {
|
||||
const selected = currentValue === item;
|
||||
return (
|
||||
<Pressable
|
||||
key={item}
|
||||
type={selected ? "selected" : "transparent"}
|
||||
onPress={() => onChange(item)}
|
||||
style={{
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: Spacing.LEVEL_2,
|
||||
borderRadius: Radius.XS,
|
||||
borderWidth: selected ? 0 : 1,
|
||||
borderColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="SM"
|
||||
lineHeight="100%"
|
||||
color={colors.primary.heading}
|
||||
>
|
||||
{dayjs().format(item)}
|
||||
</Heading>
|
||||
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
|
||||
{item}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<AppIcon
|
||||
name={selected ? "radio-button" : "ellipse"}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={
|
||||
selected
|
||||
? [colors.selected.accent, colors.static.white]
|
||||
: colors.secondary.icon
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
DateFormat.present = () => {
|
||||
presentSheet({
|
||||
component: (_ref, close) => <DateFormat close={close} />
|
||||
});
|
||||
};
|
||||
|
||||
export default DateFormat;
|
||||
378
apps/mobile/app/components/sheets/download-logs/index.tsx
Normal file
378
apps/mobile/app/components/sheets/download-logs/index.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
/*
|
||||
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 { format, logManager } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Linking, Platform, View } from "react-native";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
import ReactNativeBlobUtil from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import Share from "react-native-share";
|
||||
import { zip } from "react-native-zip-archive";
|
||||
import filesystem from "../../../common/filesystem";
|
||||
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Button } from "../../ui/button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { DatabaseLogger } from "../../../common/database";
|
||||
|
||||
type DownloadLogsProps = {
|
||||
close?: (ctx?: string) => void;
|
||||
};
|
||||
|
||||
type DownloadResult = {
|
||||
/** Local zip file path used for sharing/opening the archive. */
|
||||
localPath: string;
|
||||
/** Directory the archive was saved into (for "open location"). */
|
||||
directory: string;
|
||||
};
|
||||
|
||||
type Status = "loading" | "success" | "error";
|
||||
|
||||
const IconTile = ({
|
||||
name,
|
||||
color,
|
||||
backgroundColor
|
||||
}: {
|
||||
name: string;
|
||||
color: string;
|
||||
backgroundColor: string;
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor
|
||||
}}
|
||||
>
|
||||
<AppIcon name={name} iconFamily="notesnook" size={16} color={color} />
|
||||
</View>
|
||||
);
|
||||
|
||||
async function downloadLogs(
|
||||
onStep: (message: string, progress: number) => void
|
||||
): Promise<DownloadResult> {
|
||||
onStep(strings.preparingLogsChooseLocation(), 0.1);
|
||||
|
||||
let directory =
|
||||
Platform.OS === "ios"
|
||||
? await filesystem.checkAndCreateDir(`/debug-logs/`)
|
||||
: undefined;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
useUserStore.setState({ disableAppLockRequests: true });
|
||||
const file = await ScopedStorage.openDocumentTree(true);
|
||||
setTimeout(() => {
|
||||
useUserStore.setState({ disableAppLockRequests: false });
|
||||
}, 1000);
|
||||
if (!file) throw new Error("no-directory");
|
||||
directory = file.uri;
|
||||
}
|
||||
|
||||
if (!directory) throw new Error("no-directory");
|
||||
|
||||
onStep(strings.preparingLogsCollect(), 0.35);
|
||||
const logs = await logManager?.get();
|
||||
if (!logs) throw new Error("no-logs");
|
||||
|
||||
const logsDir = cacheDir + `/notesnook-debug-logs`;
|
||||
if (await ReactNativeBlobUtil.fs.exists(logsDir)) {
|
||||
await ReactNativeBlobUtil.fs.unlink(logsDir);
|
||||
}
|
||||
|
||||
await ReactNativeBlobUtil.fs.mkdir(logsDir);
|
||||
|
||||
for (const logGroup of logs) {
|
||||
let logString = ``;
|
||||
for (const log of logGroup.logs) {
|
||||
logString += `\n${format(log)}`;
|
||||
}
|
||||
await ReactNativeBlobUtil.fs.createFile(
|
||||
`${logsDir}/${logGroup.key}.txt`,
|
||||
logString,
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
|
||||
onStep(strings.preparingLogsCompress(), 0.7);
|
||||
const fileName = `notesnook-debug-logs-${Date.now()}.zip`;
|
||||
const outputPath = await zip(
|
||||
logsDir,
|
||||
Platform.OS === "android" ? logsDir + ".zip" : `${directory}/${fileName}`
|
||||
);
|
||||
|
||||
onStep(strings.preparingLogsSave(), 0.9);
|
||||
if (Platform.OS === "android") {
|
||||
const file = await ScopedStorage.createFile(
|
||||
directory,
|
||||
fileName,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync("file://" + outputPath, file.uri);
|
||||
// keep `outputPath` (the cache copy) around so it can be shared/opened.
|
||||
return { localPath: outputPath, directory };
|
||||
}
|
||||
|
||||
await ReactNativeBlobUtil.fs.unlink(logsDir);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
return { localPath: outputPath, directory };
|
||||
}
|
||||
|
||||
function DownloadLogs({ close }: DownloadLogsProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const [status, setStatus] = useState<Status>("loading");
|
||||
const [step, setStep] = useState({
|
||||
message: strings.preparingLogsChooseLocation(),
|
||||
progress: 0.1
|
||||
});
|
||||
const result = useRef<DownloadResult | undefined>(undefined);
|
||||
const started = useRef(false);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
setStatus("loading");
|
||||
setStep({
|
||||
message: strings.preparingLogsChooseLocation(),
|
||||
progress: 0.1
|
||||
});
|
||||
try {
|
||||
result.current = await downloadLogs((message, progress) =>
|
||||
setStep({ message, progress })
|
||||
);
|
||||
setStatus("success");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
result.current = undefined;
|
||||
setStatus("error");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (started.current) return;
|
||||
started.current = true;
|
||||
start();
|
||||
}, [start]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopLeftRadius: 35,
|
||||
borderTopRightRadius: 35,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
paddingBottom: Spacing.LEVEL_4,
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{status === "loading" ? (
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: Spacing.LEVEL_2,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<IconTile
|
||||
name="clock"
|
||||
color={colors.primary.icon}
|
||||
backgroundColor={colors.secondary.background}
|
||||
/>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.preparingLogs()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
fontSize="SM"
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{step.message}
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 8,
|
||||
borderRadius: Radius.XXL,
|
||||
backgroundColor: colors.secondary.background,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: `${Math.round(step.progress * 100)}%`,
|
||||
height: 8,
|
||||
borderRadius: Radius.XXL,
|
||||
backgroundColor: colors.primary.accent
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
) : status === "error" ? (
|
||||
<View style={{ width: "100%", gap: Spacing.LEVEL_3 }}>
|
||||
<View style={{ alignItems: "center", gap: Spacing.LEVEL_2 }}>
|
||||
<IconTile
|
||||
name="warning"
|
||||
color={colors.error.accent}
|
||||
backgroundColor={colors.error.background}
|
||||
/>
|
||||
<View style={{ gap: Spacing.LEVEL_1, width: "100%" }}>
|
||||
<Heading
|
||||
fontSize="XL"
|
||||
lineHeight="120%"
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{strings.downloadLogsFailed()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
fontSize="SM"
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{strings.downloadLogsFailedDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
title={strings.tryDownloadLogsAgain()}
|
||||
type="accent"
|
||||
width="100%"
|
||||
style={{ borderRadius: Radius.S }}
|
||||
onPress={start}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ width: "100%", gap: Spacing.LEVEL_3 }}>
|
||||
<View style={{ alignItems: "center", gap: Spacing.LEVEL_2 }}>
|
||||
<IconTile
|
||||
name="checks"
|
||||
color={colors.primary.accent}
|
||||
backgroundColor={colors.secondary.background}
|
||||
/>
|
||||
<View style={{ gap: Spacing.LEVEL_1, width: "100%" }}>
|
||||
<Heading
|
||||
fontSize="XL"
|
||||
lineHeight="120%"
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{strings.debugLogsDownloaded()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
fontSize="SM"
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{strings.debugLogsDownloadedDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ width: "100%", gap: Spacing.LEVEL_2 }}>
|
||||
<View style={{ flexDirection: "row", gap: Spacing.LEVEL_2 }}>
|
||||
<Button
|
||||
title={strings.shareZip()}
|
||||
type="plain-outline"
|
||||
style={{ flex: 1, borderRadius: Radius.S }}
|
||||
onPress={async () => {
|
||||
const path = result.current?.localPath;
|
||||
if (!path) return;
|
||||
close?.();
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
if (Platform.OS === "ios") {
|
||||
await sleep(500);
|
||||
Share.open({ url: path }).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
} else {
|
||||
FileViewer.open(path, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
} as any).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={strings.openFileLocation()}
|
||||
type="plain-outline"
|
||||
style={{ flex: 1, borderRadius: Radius.S }}
|
||||
onPress={async () => {
|
||||
const path = result.current?.localPath;
|
||||
const directory = result.current?.directory;
|
||||
if (!path) return;
|
||||
close?.();
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
if (Platform.OS === "android") {
|
||||
if (directory) {
|
||||
Linking.openURL(directory).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await sleep(500);
|
||||
FileViewer.open(path, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
title={strings.goBack()}
|
||||
type="accent"
|
||||
width="100%"
|
||||
style={{ borderRadius: Radius.S }}
|
||||
onPress={() => close?.()}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
DownloadLogs.present = () => {
|
||||
presentSheet({
|
||||
component: (_ref, close) => <DownloadLogs close={close} />
|
||||
});
|
||||
};
|
||||
|
||||
export default DownloadLogs;
|
||||
@@ -19,18 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View
|
||||
} from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { ActivityIndicator, Linking, Platform, View } from "react-native";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
import Share from "react-native-share";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { db } from "../../../common/database";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
import {
|
||||
@@ -40,16 +34,11 @@ import {
|
||||
} from "../../../services/event-manager";
|
||||
import Exporter from "../../../services/exporter";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
@@ -117,7 +106,7 @@ const ExportNotesSheet = ({
|
||||
func: async () => {
|
||||
await exportNoteAs("pdf");
|
||||
},
|
||||
icon: "file-pdf-box",
|
||||
icon: "file-pdf",
|
||||
id: notesnook.ids.dialogs.export.pdf
|
||||
},
|
||||
{
|
||||
@@ -125,7 +114,7 @@ const ExportNotesSheet = ({
|
||||
func: async () => {
|
||||
await exportNoteAs("md");
|
||||
},
|
||||
icon: "language-markdown",
|
||||
icon: "markdown",
|
||||
id: notesnook.ids.dialogs.export.md
|
||||
},
|
||||
{
|
||||
@@ -133,7 +122,7 @@ const ExportNotesSheet = ({
|
||||
func: async () => {
|
||||
await exportNoteAs("md-frontmatter");
|
||||
},
|
||||
icon: "language-markdown",
|
||||
icon: "markdown",
|
||||
id: notesnook.ids.dialogs.export.md
|
||||
},
|
||||
{
|
||||
@@ -141,7 +130,7 @@ const ExportNotesSheet = ({
|
||||
func: async () => {
|
||||
await exportNoteAs("txt");
|
||||
},
|
||||
icon: "card-text",
|
||||
icon: "file-text",
|
||||
id: notesnook.ids.dialogs.export.text
|
||||
},
|
||||
{
|
||||
@@ -149,215 +138,222 @@ const ExportNotesSheet = ({
|
||||
func: async () => {
|
||||
await exportNoteAs("html");
|
||||
},
|
||||
icon: "language-html5",
|
||||
icon: "file-html",
|
||||
id: notesnook.ids.dialogs.export.html
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopLeftRadius: 35,
|
||||
borderTopRightRadius: 35,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<Dialog context="export-notes" />
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="export"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{complete
|
||||
? strings.exportSuccessHeading(ids.length)
|
||||
: strings.exportNotes(ids.length)}
|
||||
</Heading>
|
||||
<Paragraph>
|
||||
{complete
|
||||
? strings.exportSuccessDesc(result?.fileName as string)
|
||||
: strings.exportAllNotesDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: colors.primary.border
|
||||
}}
|
||||
/>
|
||||
|
||||
{!complete && !exporting ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
icon="export"
|
||||
title={strings.exportNotes(ids.length)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Seperator half />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Dialog context="export-notes" />
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
{!exporting && !complete ? (
|
||||
actions.map((item) => (
|
||||
<Fragment key={item.title}>
|
||||
<Seperator half />
|
||||
{actions.map((item) => (
|
||||
<Pressable
|
||||
key={item.title}
|
||||
testID={item.id}
|
||||
type="transparent"
|
||||
onPress={item.func}
|
||||
style={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
paddingRight: 12,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: 0,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
borderRadius: Radius.S
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.shade,
|
||||
borderRadius: defaultBorderRadius,
|
||||
height: 60,
|
||||
width: 60,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
<AppIcon
|
||||
name={item.icon}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
size={AppFontSize.xxxl + 10}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading style={{ marginLeft: 10 }} size={AppFontSize.md}>
|
||||
{item.title}
|
||||
</Heading>
|
||||
{/* <Paragraph
|
||||
style={{ marginLeft: 10 }}
|
||||
size={SIZE.sm}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{item.desc}
|
||||
</Paragraph> */}
|
||||
</View>
|
||||
</Pressable>
|
||||
</Fragment>
|
||||
))
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
minHeight: 150,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
{!complete ? (
|
||||
<>
|
||||
<ActivityIndicator />
|
||||
<Paragraph>
|
||||
{strings.exportingNotes(status) +
|
||||
"..." +
|
||||
" " +
|
||||
strings.pleaseWait()}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconButton
|
||||
name="export"
|
||||
color={colors.primary.icon}
|
||||
size={50}
|
||||
style={{
|
||||
width: 70,
|
||||
height: 70
|
||||
}}
|
||||
/>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
color={colors.secondary.heading}
|
||||
>
|
||||
{strings.exportSuccessHeading(ids.length)}
|
||||
<Heading fontSize="MD" lineHeight="100%">
|
||||
{item.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{strings.exportSuccessDesc(result?.fileName as string)}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={
|
||||
Platform.OS === "android"
|
||||
? strings.openFileLocation()
|
||||
: strings.open()
|
||||
}
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!result?.filePath) return;
|
||||
close?.();
|
||||
if (Platform.OS === "android") {
|
||||
Linking.openURL(result.fileDir).catch((e) => {
|
||||
ToastManager.error(e as Error);
|
||||
});
|
||||
} else {
|
||||
await sleep(500);
|
||||
FileViewer.open(result?.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
}).catch((e) => {
|
||||
ToastManager.show({
|
||||
heading: strings.noApplicationFound(result.name),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={strings.share()}
|
||||
type="secondaryAccented"
|
||||
width={250}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!result) return;
|
||||
close?.();
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
if (Platform.OS === "ios") {
|
||||
await sleep(500);
|
||||
Share.open({
|
||||
url: result?.fileDir + result.fileName
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
} else {
|
||||
FileViewer.open(result.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
} as any).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={strings.exportAgain()}
|
||||
type="secondaryAccented"
|
||||
width={250}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
onPress={async () => {
|
||||
setComplete(false);
|
||||
setResult(undefined);
|
||||
setExporting(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
minHeight: 150,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
paddingVertical: Spacing.LEVEL_3,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{!complete ? (
|
||||
<>
|
||||
<ActivityIndicator color={colors.primary.accent} />
|
||||
<Paragraph>
|
||||
{strings.exportingNotes(status) +
|
||||
"..." +
|
||||
" " +
|
||||
strings.pleaseWait()}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
title={
|
||||
Platform.OS === "android"
|
||||
? strings.openFileLocation()
|
||||
: strings.open()
|
||||
}
|
||||
type="accent"
|
||||
width={"100%"}
|
||||
onPress={async () => {
|
||||
if (!result?.filePath) return;
|
||||
close?.();
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
if (Platform.OS === "android") {
|
||||
Linking.openURL(result.fileDir).catch((e) => {
|
||||
ToastManager.error(e as Error);
|
||||
});
|
||||
} else {
|
||||
await sleep(500);
|
||||
FileViewer.open(result?.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
}).catch(() => {
|
||||
ToastManager.show({
|
||||
heading: strings.noApplicationFound(result.name),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={strings.share()}
|
||||
type="secondary-simple"
|
||||
width={"100%"}
|
||||
onPress={async () => {
|
||||
if (!result) return;
|
||||
close?.();
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
if (Platform.OS === "ios") {
|
||||
await sleep(500);
|
||||
Share.open({
|
||||
url: result?.fileDir + result.fileName
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
} else {
|
||||
FileViewer.open(result.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
} as any).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={strings.exportAgain()}
|
||||
type="plain-outline"
|
||||
width={"100%"}
|
||||
onPress={async () => {
|
||||
setComplete(false);
|
||||
setResult(undefined);
|
||||
setExporting(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -372,38 +368,4 @@ ExportNotesSheet.present = async (ids?: string[], allNotes?: boolean) => {
|
||||
});
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
...getElevationStyle(5),
|
||||
borderRadius: defaultBorderRadius,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
},
|
||||
buttonContainer: {
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
},
|
||||
button: {
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderRadius: defaultBorderRadius,
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
flexDirection: "row"
|
||||
},
|
||||
buttonText: {
|
||||
//fontFamily: "sans-serif",
|
||||
color: "white",
|
||||
fontSize: AppFontSize.sm,
|
||||
marginLeft: 5
|
||||
},
|
||||
overlay: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute"
|
||||
}
|
||||
});
|
||||
|
||||
export default ExportNotesSheet;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ToastManager } from "../../../services/event-manager";
|
||||
import { Button } from "../../ui/button";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
export const ShareComponent = ({ uri, name, padding }) => {
|
||||
return (
|
||||
<View
|
||||
@@ -36,6 +37,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
|
||||
type="accent"
|
||||
width="100%"
|
||||
onPress={async () => {
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
FileViewer.open(uri, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
@@ -56,6 +58,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
onPress={async () => {
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
FileViewer.open(uri, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
|
||||
221
apps/mobile/app/components/sheets/force-sync/index.tsx
Normal file
221
apps/mobile/app/components/sheets/force-sync/index.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { Button } from "../../ui/button";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { presentSheet, eSendEvent } from "../../../services/event-manager";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { Progress } from "../progress";
|
||||
import Sync from "../../../services/sync";
|
||||
|
||||
type ForceSyncMode = "fetch" | "send";
|
||||
|
||||
type ForceSyncProps = {
|
||||
mode: ForceSyncMode;
|
||||
close?: (ctx?: string) => void;
|
||||
};
|
||||
|
||||
function ForceSync({ mode, close }: ForceSyncProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const [acknowledged, setAcknowledged] = useState(true);
|
||||
|
||||
const onForceSyncPress = async () => {
|
||||
if (!acknowledged) return;
|
||||
|
||||
close?.();
|
||||
await sleep(300);
|
||||
Progress.present();
|
||||
Sync.run("global", true, mode, () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
});
|
||||
};
|
||||
|
||||
const title =
|
||||
mode === "fetch" ? strings.forcePullChanges() : strings.forcePushChanges();
|
||||
const description =
|
||||
mode === "fetch"
|
||||
? strings.forceSyncPullSheetDesc()
|
||||
: strings.forceSyncPushSheetDesc();
|
||||
const actionText =
|
||||
mode === "fetch" ? strings.forcePullAction() : strings.forcePushAction();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopLeftRadius: 35,
|
||||
borderTopRightRadius: 35,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.error.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="warning"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.error.accent}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.lg} lineHeight="100%">
|
||||
{title}
|
||||
</Heading>
|
||||
<Paragraph size={AppFontSize.xs}>{description}</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: colors.primary.border
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>
|
||||
{strings.forceSyncWarningShort()}
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>
|
||||
{strings.forceSyncNeedHelpContact()}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
fontFamily="SEMI_BOLD"
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
support@streetwriters.co
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
type="shade"
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: Radius.XS,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1,
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
onPress={() => setAcknowledged((value) => !value)}
|
||||
>
|
||||
<AppIcon
|
||||
size={16}
|
||||
name={acknowledged ? "checkbox" : "checkbox-blank-outline"}
|
||||
iconFamily="notesnook"
|
||||
color={
|
||||
acknowledged
|
||||
? [colors.primary.accent, "white"]
|
||||
: colors.primary.icon
|
||||
}
|
||||
/>
|
||||
<Paragraph size={AppFontSize.sm}>
|
||||
{strings.forceSyncRiskAcknowledgement()}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="plain-outline"
|
||||
width="100%"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: Radius.S
|
||||
}}
|
||||
onPress={() => close?.()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={actionText}
|
||||
type="error"
|
||||
width="100%"
|
||||
disabled={!acknowledged}
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: Radius.S,
|
||||
borderColor: colors.error.border
|
||||
}}
|
||||
onPress={onForceSyncPress}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
ForceSync.present = (mode: ForceSyncMode) => {
|
||||
presentSheet({
|
||||
disableClosing: false,
|
||||
component: (_ref, close) => <ForceSync mode={mode} close={close} />
|
||||
});
|
||||
};
|
||||
|
||||
export default ForceSync;
|
||||
@@ -18,26 +18,33 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Debug, IssueReportResponse } from "@notesnook/core";
|
||||
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Linking, Platform, Text, TextInput, View } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import { Linking, Platform, View } from "react-native";
|
||||
import Config from "react-native-config";
|
||||
import {
|
||||
getBrand,
|
||||
getModel,
|
||||
getSystemVersion,
|
||||
getVersion
|
||||
} from "react-native-device-info";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { useStoredRef } from "../../../hooks/use-stored-ref";
|
||||
import { eSendEvent, ToastManager } from "../../../services/event-manager";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size/index";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import {
|
||||
createFormRef,
|
||||
FormInput,
|
||||
validators
|
||||
} from "../../ui/input/form-input";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import Config from "react-native-config";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
|
||||
export const Issue = ({
|
||||
defaultTitle,
|
||||
@@ -54,22 +61,27 @@ export const Issue = ({
|
||||
const [done, setDone] = useState(false);
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const bodyRef = useRef<TextInput>(null);
|
||||
const initialLayout = useRef(false);
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
title: title.current || "",
|
||||
body: body.current || ""
|
||||
})
|
||||
);
|
||||
const issueReportResponse = useRef<IssueReportResponse>(undefined);
|
||||
|
||||
const onPress = async () => {
|
||||
if (loading) return;
|
||||
if (!title.current || !body.current) return;
|
||||
if (title.current?.trim() === "" || body.current?.trim().length === 0)
|
||||
return;
|
||||
if (!formRef.current.validate()) return;
|
||||
|
||||
const titleValue = formRef.current.getValue("title").trim();
|
||||
const bodyValue = formRef.current.getValue("body").trim();
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
issueReportResponse.current = await Debug.report({
|
||||
title: title.current,
|
||||
title: titleValue,
|
||||
body:
|
||||
body.current +
|
||||
bodyValue +
|
||||
`\n${defaultBody || ""}` +
|
||||
`\n_______________
|
||||
**Device information:**
|
||||
@@ -93,6 +105,8 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
|
||||
setLoading(false);
|
||||
body.reset();
|
||||
title.reset();
|
||||
formRef.current.setValue("title", "");
|
||||
formRef.current.setValue("body", "");
|
||||
setDone(true);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
@@ -135,161 +149,195 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
|
||||
|
||||
const responseInfo = getResponseInfo(issueReportResponse.current);
|
||||
|
||||
console.log(responseInfo, issueReportResponse.current);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopLeftRadius: 35,
|
||||
borderTopRightRadius: 35,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{done ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<Heading>{responseInfo?.title}</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
selectable={true}
|
||||
>
|
||||
{responseInfo?.message}
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={responseInfo?.positiveButtonText || strings.done()}
|
||||
onPress={() => {
|
||||
if (responseInfo?.url) {
|
||||
Linking.openURL(responseInfo?.url);
|
||||
}
|
||||
eSendEvent(eCloseSheet);
|
||||
}}
|
||||
type="accent"
|
||||
width="100%"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading>{responseInfo?.title}</Heading>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.error.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="warning-circle"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.error.accent}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{issueTitle || strings.issueTitle()}
|
||||
</Heading>
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{issueTitle ? strings.issueDesc() : strings.issueDesc2()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: colors.primary.border
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
name="title"
|
||||
formRef={formRef}
|
||||
label={strings.issueSummary()}
|
||||
placeholder={strings.issueTitlePlaceholder()}
|
||||
defaultValue={title.current}
|
||||
validators={[validators.required(strings.allFieldsRequired())]}
|
||||
onChangeText={(v) => (title.current = v)}
|
||||
containerStyle={{
|
||||
borderRadius: Radius.XS
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
name="body"
|
||||
formRef={formRef}
|
||||
label={strings.details()}
|
||||
placeholder={strings.issuePlaceholder()}
|
||||
multiline
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
validators={[validators.required(strings.allFieldsRequired())]}
|
||||
onChangeText={(v) => (body.current = v)}
|
||||
containerStyle={{
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
inputStyle={{
|
||||
minHeight: 100
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={loading ? null : strings.submit()}
|
||||
loading={loading}
|
||||
width="100%"
|
||||
type="error"
|
||||
style={{
|
||||
borderRadius: Radius.S,
|
||||
borderColor: colors.error.border
|
||||
}}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
fontSize="XS"
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
selectable={true}
|
||||
>
|
||||
{responseInfo?.message}
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={responseInfo?.positiveButtonText || "Done"}
|
||||
onPress={() => {
|
||||
if (responseInfo?.url) {
|
||||
Linking.openURL(responseInfo?.url);
|
||||
}
|
||||
eSendEvent(eCloseSheet);
|
||||
}}
|
||||
type="accent"
|
||||
width="100%"
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader
|
||||
title={issueTitle || strings.issueTitle()}
|
||||
paragraph={issueTitle ? strings.issueDesc() : strings.issueDesc2()}
|
||||
/>
|
||||
|
||||
<Seperator half />
|
||||
|
||||
<TextInput
|
||||
placeholder={strings.title()}
|
||||
onChangeText={(v) => (title.current = v)}
|
||||
defaultValue={title.current}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: defaultBorderRadius,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
fontFamily: "Inter-Regular",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
fontSize: AppFontSize.md,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
ref={bodyRef}
|
||||
multiline
|
||||
placeholder={strings.issuePlaceholder()}
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
onChangeText={(v) => (body.current = v)}
|
||||
onLayout={() => {
|
||||
if (initialLayout.current) return;
|
||||
initialLayout.current = true;
|
||||
if (body.current) {
|
||||
bodyRef.current?.setNativeProps({
|
||||
text: body.current,
|
||||
selection: {
|
||||
start: 0,
|
||||
end: 0
|
||||
{strings.issueNotice[0]()}{" "}
|
||||
<Paragraph
|
||||
onPress={() => {
|
||||
Linking.openURL(
|
||||
"https://github.com/streetwriters/notesnook/issues"
|
||||
);
|
||||
}}
|
||||
fontSize="XS"
|
||||
fontFamily="MEDIUM"
|
||||
style={{
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
Github.
|
||||
</Paragraph>{" "}
|
||||
{strings.issueNotice[1]()}{" "}
|
||||
<Paragraph
|
||||
style={{
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
fontSize="XS"
|
||||
fontFamily="MEDIUM"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser("https://discord.gg/zQBK97EE22");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: defaultBorderRadius,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
fontFamily: "Inter-Regular",
|
||||
maxHeight: 200,
|
||||
fontSize: AppFontSize.sm,
|
||||
marginBottom: 2.5,
|
||||
color: colors.primary.paragraph
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>{`App version: ${getVersion()} Platform: ${
|
||||
Platform.OS
|
||||
} Model: ${getBrand()}-${getModel()}-${getSystemVersion()}`}</Paragraph>
|
||||
|
||||
<Seperator />
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={loading ? null : strings.submit()}
|
||||
loading={loading}
|
||||
width="100%"
|
||||
type="accent"
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xs}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{strings.issueNotice[0]()}{" "}
|
||||
<Text
|
||||
onPress={() => {
|
||||
Linking.openURL(
|
||||
"https://github.com/streetwriters/notesnook/issues"
|
||||
);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
github.com/streetwriters/notesnook.
|
||||
</Text>{" "}
|
||||
{strings.issueNotice[1]()}{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser("https://discord.gg/zQBK97EE22");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{strings.issueNotice[2]()}
|
||||
</Text>
|
||||
</Paragraph>
|
||||
}}
|
||||
>
|
||||
{strings.issueNotice[2]()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
188
apps/mobile/app/components/sheets/lock-vault-timer/index.tsx
Normal file
188
apps/mobile/app/components/sheets/lock-vault-timer/index.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { useWindowDimensions, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
const OPTIONS = [
|
||||
1000 * 60 * 1,
|
||||
1000 * 60 * 5,
|
||||
1000 * 60 * 10,
|
||||
1000 * 60 * 15,
|
||||
1000 * 60 * 30,
|
||||
1000 * 60 * 45,
|
||||
1000 * 60 * 60,
|
||||
-1
|
||||
];
|
||||
|
||||
const formatValue = (item: number) => {
|
||||
return item === -1
|
||||
? strings.never()
|
||||
: item < 1000 * 60 * 60
|
||||
? strings.minutes(item / (1000 * 60))
|
||||
: strings.hours(item / (1000 * 60 * 60));
|
||||
};
|
||||
|
||||
type LockVaultTimerProps = {
|
||||
close?: (ctx?: string) => void;
|
||||
};
|
||||
|
||||
function LockVaultTimer({ close }: LockVaultTimerProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useWindowDimensions();
|
||||
const [currentValue, setCurrentValue] = useState(
|
||||
useSettingStore.getState().vaultLockAfter
|
||||
);
|
||||
|
||||
const onChange = async (item: number) => {
|
||||
await db.settings.setVaultLockAfter(item);
|
||||
useSettingStore.setState({
|
||||
vaultLockAfter: item
|
||||
});
|
||||
setCurrentValue(item);
|
||||
close?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{
|
||||
maxHeight: height * 0.8
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1,
|
||||
paddingTop: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="clock"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.lockVaultAfter()}
|
||||
</Heading>
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{strings.lockVaultAfterDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopLeftRadius: 35,
|
||||
borderTopRightRadius: 35,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{OPTIONS.map((item) => {
|
||||
const selected = currentValue === item;
|
||||
return (
|
||||
<Pressable
|
||||
key={item}
|
||||
type={selected ? "selected" : "transparent"}
|
||||
onPress={() => onChange(item)}
|
||||
style={{
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: Spacing.LEVEL_2,
|
||||
borderRadius: Radius.XS,
|
||||
borderWidth: selected ? 0 : 1,
|
||||
borderColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="SM"
|
||||
lineHeight="100%"
|
||||
color={colors.primary.heading}
|
||||
>
|
||||
{formatValue(item)}
|
||||
</Heading>
|
||||
|
||||
<AppIcon
|
||||
name={selected ? "radio-button" : "ellipse"}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={
|
||||
selected
|
||||
? [colors.selected.accent, colors.static.white]
|
||||
: colors.secondary.icon
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
LockVaultTimer.present = () => {
|
||||
presentSheet({
|
||||
component: (_ref, close) => <LockVaultTimer close={close} />
|
||||
});
|
||||
};
|
||||
|
||||
export default LockVaultTimer;
|
||||
@@ -37,8 +37,8 @@ import PremiumService from "../../../services/premium";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { FontSizes } from "../../../common/design/font";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { AuthMode } from "../../auth/common";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Button } from "../../ui/button";
|
||||
@@ -87,181 +87,188 @@ export default function PaywallSheet<Tid extends FeatureId>(props: {
|
||||
SubscriptionProvider.GOOGLE &&
|
||||
Platform.OS === "android");
|
||||
|
||||
return !pricingPlans.currentPlan ? null : (
|
||||
if (!pricingPlans.currentPlan) return null;
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: `${
|
||||
PlanOverView[pricingPlans.currentPlan.id as keyof typeof PlanOverView]
|
||||
.storage
|
||||
} ${strings.cloudStorage()}`,
|
||||
description: strings.cloudStorageBenefit()
|
||||
},
|
||||
...(pricingPlans.currentPlan.id !== "essential"
|
||||
? [
|
||||
{
|
||||
title: strings.appLockSecurity(),
|
||||
description: strings.appLockSecurityBenefit()
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: strings.advancedNoteElements(),
|
||||
description: strings.advancedNoteElementsBenefit()
|
||||
}
|
||||
];
|
||||
|
||||
const onUpgrade = () => {
|
||||
if (PremiumService.get()) {
|
||||
if (
|
||||
pricingPlans.user?.subscription.plan === SubscriptionPlan.LEGACY_PRO ||
|
||||
!isCurrentPlatform
|
||||
) {
|
||||
ToastManager.show({
|
||||
message: strings.cannotChangePlan(),
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubscribedOnWeb) {
|
||||
ToastManager.show({
|
||||
message: strings.changePlanOnWeb(),
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
eSendEvent(eCloseSheet);
|
||||
if (!useUserStore.getState().user) {
|
||||
Navigation.navigate("Auth", {
|
||||
mode: AuthMode.login
|
||||
});
|
||||
return;
|
||||
}
|
||||
Navigation.navigate("PayWall", {
|
||||
context: "logged-in",
|
||||
state: {
|
||||
planId: pricingPlans.currentPlan?.id,
|
||||
productId: isGithubRelease
|
||||
? "yearly"
|
||||
: (pricingPlans.selectProduct as any).productId,
|
||||
billingType: "annual"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: Spacing.LEVEL_4,
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<View style={{ gap: Spacing.LEVEL_3, width: "100%" }}>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Paragraph>
|
||||
<AppIcon
|
||||
name="crown"
|
||||
size={AppFontSize.md}
|
||||
color={colors.static.orange}
|
||||
/>
|
||||
{strings.upgradePlanTo(pricingPlans.currentPlan?.name)}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
width: "100%"
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<Heading>{strings.tryItForFree()}</Heading>
|
||||
|
||||
<Heading size={AppFontSize.sm}>
|
||||
{strings.getThisAndSoMuchMore()}
|
||||
<AppIcon
|
||||
name="crown-simple"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.static.orange}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.tryItForFree()}
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<AppIcon name="cloud" size={AppFontSize.xxl} />
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.sm}>
|
||||
{
|
||||
PlanOverView[
|
||||
pricingPlans.currentPlan.id as keyof typeof PlanOverView
|
||||
].storage
|
||||
}
|
||||
</Heading>{" "}
|
||||
{strings.cloudSpace()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{pricingPlans.currentPlan.id !== "essential" ? (
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<AppIcon name="lock" size={AppFontSize.xxl} />
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.sm}>{strings.appLock()}</Heading>{" "}
|
||||
{strings.appLockFeatureBenefit()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<AppIcon name="vector-link" size={AppFontSize.xxl} />
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{strings.advancedNoteTaking[0]()}{" "}
|
||||
<Heading size={AppFontSize.sm}>
|
||||
{strings.advancedNoteTaking[1]()}
|
||||
</Heading>{" "}
|
||||
{strings.advancedNoteTaking[2]()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<Paragraph fontSize="SM" color={colors.primary.paragraph}>
|
||||
{strings.unlockPremiumFeatures()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
marginVertical: 10
|
||||
}}
|
||||
size={AppFontSize.xs}
|
||||
>
|
||||
<Heading size={AppFontSize.xs}>{strings.cancelAnytime()}</Heading>{" "}
|
||||
{strings.googleReminderTrial()}
|
||||
</Paragraph>
|
||||
<View style={{ height: 1, backgroundColor: colors.primary.border }} />
|
||||
|
||||
<Button
|
||||
type="accent"
|
||||
title={strings.upgrade()}
|
||||
style={{
|
||||
marginVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (PremiumService.get()) {
|
||||
if (
|
||||
pricingPlans.user?.subscription.plan ===
|
||||
SubscriptionPlan.LEGACY_PRO ||
|
||||
!isCurrentPlatform
|
||||
) {
|
||||
ToastManager.show({
|
||||
message: strings.cannotChangePlan(),
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubscribedOnWeb) {
|
||||
ToastManager.show({
|
||||
message: strings.changePlanOnWeb(),
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
eSendEvent(eCloseSheet);
|
||||
if (!useUserStore.getState().user) {
|
||||
Navigation.navigate("Auth", {
|
||||
mode: AuthMode.login
|
||||
});
|
||||
return;
|
||||
}
|
||||
Navigation.navigate("PayWall", {
|
||||
context: "logged-in",
|
||||
state: {
|
||||
planId: pricingPlans.currentPlan?.id,
|
||||
productId: isGithubRelease
|
||||
? "yearly"
|
||||
: (pricingPlans.selectProduct as any).productId,
|
||||
billingType: "annual"
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<View style={{ gap: Spacing.LEVEL_2, width: "100%" }}>
|
||||
{features.map((feature) => (
|
||||
<View
|
||||
key={feature.title}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="check"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.accent}
|
||||
style={{ marginTop: 1 }}
|
||||
/>
|
||||
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
|
||||
<Heading fontFamily="MEDIUM" fontSize="SM" lineHeight="100%">
|
||||
{feature.title}
|
||||
</Heading>
|
||||
<Paragraph fontSize="XS" color={colors.primary.paragraph}>
|
||||
{feature.description}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_0 + 2,
|
||||
padding: Spacing.LEVEL_1,
|
||||
borderRadius: Radius.XS,
|
||||
backgroundColor: colors.selected.background
|
||||
}}
|
||||
>
|
||||
<Paragraph fontSize="XS" color={colors.secondary.heading}>
|
||||
{strings.sevenDayFreeTrial()}
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: Radius.MD,
|
||||
backgroundColor: colors.secondary.heading
|
||||
}}
|
||||
/>
|
||||
<Paragraph fontSize="XS" color={colors.secondary.heading}>
|
||||
{strings.cancelAnytimeShort()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{ flexDirection: "row", gap: Spacing.LEVEL_2, width: "100%" }}
|
||||
>
|
||||
{isSubscribedOnWeb ? null : (
|
||||
<Button
|
||||
type="plain"
|
||||
title={strings.exploreAllPlans()}
|
||||
icon="arrow-right"
|
||||
iconPosition="right"
|
||||
type="plain-outline"
|
||||
title={strings.viewPlans()}
|
||||
fontSize={FontSizes.MD}
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: Radius.S,
|
||||
paddingVertical: Spacing.LEVEL_3
|
||||
}}
|
||||
onPress={() => {
|
||||
eSendEvent(eCloseSheet);
|
||||
Navigation.navigate("PayWall", {
|
||||
@@ -272,7 +279,18 @@ export default function PaywallSheet<Tid extends FeatureId>(props: {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<Button
|
||||
type="accent"
|
||||
title={strings.upgrade()}
|
||||
fontSize={FontSizes.MD}
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: Radius.S,
|
||||
paddingVertical: Spacing.LEVEL_3
|
||||
}}
|
||||
onPress={onUpgrade}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,30 +16,40 @@ 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 React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
FeatureId,
|
||||
FeatureUsage,
|
||||
formatBytes,
|
||||
getFeature,
|
||||
getFeaturesUsage
|
||||
} from "@notesnook/common";
|
||||
import { SubscriptionPlan, SubscriptionProvider } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { FontSizes } from "../../../common/design/font";
|
||||
import { Spacing, Radius } from "../../../common/design/spacing";
|
||||
import { eSendEvent, ToastManager } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Button } from "../../ui/button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { SubscriptionPlan, SubscriptionProvider } from "@notesnook/core";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import SettingsService from "../../../services/settings";
|
||||
|
||||
const FEATURE_ICONS: Partial<Record<FeatureId, string>> = {
|
||||
storage: "cloud",
|
||||
colors: "palette",
|
||||
tags: "shopping-mode",
|
||||
notebooks: "book-open",
|
||||
activeReminders: "bell",
|
||||
shortcuts: "arrow-square-out"
|
||||
};
|
||||
|
||||
export function PlanLimits() {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -63,46 +73,97 @@ export function PlanLimits() {
|
||||
return (
|
||||
<ScrollView
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
width: "100%",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: Spacing.LEVEL_4
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<Heading>{strings.planLimits()}</Heading>
|
||||
|
||||
{featureUsage?.map((item) => (
|
||||
<View style={{ gap: Spacing.LEVEL_3, width: "100%" }}>
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between"
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>
|
||||
{getFeature(item.id).title}
|
||||
</Paragraph>
|
||||
<Paragraph size={AppFontSize.sm}>
|
||||
{item.total === Infinity
|
||||
? strings.unlimited()
|
||||
: item.id === "storage"
|
||||
? `${formatBytes(item.used)}/${formatBytes(
|
||||
item.total
|
||||
)} ${strings.used()}`
|
||||
: `${item.used}/${item.total} ${strings.used()}`}
|
||||
<AppIcon
|
||||
name="chart-donut"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.planLimits()}
|
||||
</Heading>
|
||||
<Paragraph fontSize="SM" color={colors.primary.paragraph}>
|
||||
{strings.planLimitsDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={{ height: 1, backgroundColor: colors.primary.border }} />
|
||||
|
||||
<View style={{ gap: Spacing.LEVEL_2, width: "100%" }}>
|
||||
{featureUsage?.map((item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name={FEATURE_ICONS[item.id] || "checkbox"}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<Paragraph fontSize="SM" color={colors.primary.paragraph}>
|
||||
{getFeature(item.id).title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<Paragraph
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="XS"
|
||||
color={colors.secondary.heading}
|
||||
>
|
||||
{item.total === Infinity
|
||||
? strings.unlimited()
|
||||
: item.id === "storage"
|
||||
? `${formatBytes(item.used)}/${formatBytes(
|
||||
item.total
|
||||
)} ${strings.used()}`
|
||||
: `${item.used}/${item.total} ${strings.used()}`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{((user?.subscription?.provider === SubscriptionProvider.PADDLE ||
|
||||
user?.subscription?.provider === SubscriptionProvider.STREETWRITERS ||
|
||||
@@ -139,10 +200,10 @@ export function PlanLimits() {
|
||||
eSendEvent(eCloseSheet);
|
||||
}}
|
||||
type="accent"
|
||||
fontSize={AppFontSize.xs}
|
||||
fontSize={FontSizes.MD}
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
borderRadius: Radius.S
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -198,7 +198,7 @@ const PublishNoteSheet = ({
|
||||
};
|
||||
|
||||
const monographMetadata =
|
||||
monographData.result ?? lastMonographDataResult.current;
|
||||
monographData.result?.metadata ?? lastMonographDataResult.current;
|
||||
|
||||
useEffect(() => {
|
||||
const prevState = previousAppState.current;
|
||||
@@ -290,7 +290,7 @@ const PublishNoteSheet = ({
|
||||
{strings.publishedAt()}:
|
||||
</Heading>
|
||||
<Paragraph size={AppFontSize.sm} numberOfLines={1}>
|
||||
{publishUrl}
|
||||
{monographMetadata?.publishUrl}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -432,7 +432,7 @@ const PublishNoteSheet = ({
|
||||
{isFeatureAvailable?.isAllowed &&
|
||||
!selfDestruct &&
|
||||
monographMetadata &&
|
||||
monographMetadata?.totalViews > 0 ? (
|
||||
monographMetadata?.analytics.totalViews > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -456,7 +456,9 @@ const PublishNoteSheet = ({
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
|
||||
<Paragraph>{monographMetadata?.totalViews || 0}</Paragraph>
|
||||
<Paragraph>
|
||||
{monographMetadata?.analytics.totalViews || 0}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { createRef } from "react";
|
||||
import { PermissionsAndroid, Platform, View } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import Share from "react-native-share";
|
||||
import { db } from "../../../common/database";
|
||||
import filesystem from "../../../common/filesystem";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastManager
|
||||
} from "../../../services/event-manager";
|
||||
import { clearMessage } from "../../../services/message";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { eOpenRecoveryKeyDialog } from "../../../utils/events";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import { QRCode } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
|
||||
|
||||
class RecoveryKeySheet extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
key: null,
|
||||
visible: false
|
||||
};
|
||||
this.actionSheetRef = createRef();
|
||||
this.svg = createRef();
|
||||
this.user;
|
||||
this.signup = false;
|
||||
this.tapCount = 0;
|
||||
}
|
||||
|
||||
open = (signup) => {
|
||||
if (signup) {
|
||||
this.signup = true;
|
||||
}
|
||||
this.setState(
|
||||
{
|
||||
visible: true
|
||||
},
|
||||
() => {
|
||||
this.actionSheetRef.current?.setModalVisible(true);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
close = () => {
|
||||
if (this.tapCount === 0) {
|
||||
ToastManager.show({
|
||||
heading: strings.recoveryKeySaved(),
|
||||
message: strings.recoveryKeySavedDesc(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
this.tapCount++;
|
||||
return;
|
||||
}
|
||||
this.tapCount = 0;
|
||||
this.actionSheetRef.current?.setModalVisible(false);
|
||||
sleep(200).then(() => {
|
||||
this.setState({
|
||||
visible: false
|
||||
});
|
||||
});
|
||||
SettingsService.set({
|
||||
recoveryKeySaved: true
|
||||
});
|
||||
clearMessage();
|
||||
};
|
||||
async componentDidMount() {
|
||||
eSubscribeEvent(eOpenRecoveryKeyDialog, this.open);
|
||||
}
|
||||
|
||||
async componentWillUnmount() {
|
||||
eUnSubscribeEvent(eOpenRecoveryKeyDialog, this.open);
|
||||
}
|
||||
|
||||
saveQRCODE = async () => {
|
||||
this.svg.current?.toDataURL(async (data) => {
|
||||
try {
|
||||
let fileName =
|
||||
"nn_" + this.user.email + "_recovery_key_qrcode" + "_" + Date.now();
|
||||
fileName = sanitizeFilename(fileName, { replacement: "_" });
|
||||
fileName = fileName + ".png";
|
||||
|
||||
const path = RNFetchBlob.fs.dirs.CacheDir + fileName;
|
||||
await RNFetchBlob.fs.writeFile(path, data, "base64");
|
||||
await CameraRoll.saveToCameraRoll(`file://` + path);
|
||||
ToastManager.show({
|
||||
heading: strings.recoveryKeyQRCodeSaved(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
saveToTextFile = async () => {
|
||||
try {
|
||||
let path;
|
||||
let fileName = "nn_" + this.user?.email + "_recovery_key";
|
||||
fileName = sanitizeFilename(fileName, { replacement: "_" });
|
||||
fileName = fileName + ".txt";
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
let file = await ScopedStorage.createDocument(
|
||||
fileName,
|
||||
"text/plain",
|
||||
this.state.key,
|
||||
"utf8"
|
||||
);
|
||||
if (!file) return;
|
||||
path = file.uri;
|
||||
} else {
|
||||
path = await filesystem.checkAndCreateDir("/");
|
||||
await RNFetchBlob.fs.writeFile(path + fileName, this.state.key, "utf8");
|
||||
path = path + fileName;
|
||||
}
|
||||
|
||||
ToastManager.show({
|
||||
heading: strings.recoveryKeyTextFileSaved(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
return path;
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
onOpen = async () => {
|
||||
let k = await db.user.getMasterKey();
|
||||
this.user = await db.user.getUser();
|
||||
if (k) {
|
||||
this.setState({
|
||||
key: k.key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
shareFile = async () => {
|
||||
let path = await this.saveToTextFile();
|
||||
if (!path) return;
|
||||
try {
|
||||
if (Platform.OS === "ios") {
|
||||
Share.open({
|
||||
url: path,
|
||||
failOnCancel: false
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
} else {
|
||||
FileViewer.open(path, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { colors } = this.props;
|
||||
if (!this.state.visible) return null;
|
||||
return (
|
||||
<SheetWrapper
|
||||
closeOnTouchBackdrop={false}
|
||||
gestureEnabled={false}
|
||||
onOpen={this.onOpen}
|
||||
fwdRef={this.actionSheetRef}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderRadius: 10,
|
||||
paddingTop: 10
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={strings.saveRecoveryKey()}
|
||||
paragraph={strings.saveRecoveryKeyDesc()}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
borderRadius: defaultBorderRadius,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
color={colors.primary.paragraph}
|
||||
size={AppFontSize.sm}
|
||||
numberOfLines={2}
|
||||
selectable
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
paddingRight: 10,
|
||||
textAlign: "center",
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
>
|
||||
{this.state.key}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<Seperator />
|
||||
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginBottom: 15,
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
position: "absolute",
|
||||
opacity: 0,
|
||||
zIndex: -1
|
||||
}}
|
||||
>
|
||||
{this.state.key ? (
|
||||
<QRCode
|
||||
getRef={this.svg}
|
||||
size={500}
|
||||
value={this.state.key}
|
||||
//logo={{ uri: LOGO_BASE64 }}
|
||||
logoBorderRadius={10}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
Clipboard.setString(this.state.key);
|
||||
ToastManager.show({
|
||||
heading: strings.recoveryKeyCopied(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
icon="content-copy"
|
||||
title={strings.copyToClipboard()}
|
||||
width="100%"
|
||||
type="secondaryAccented"
|
||||
/>
|
||||
<Seperator />
|
||||
<Button
|
||||
title={strings.saveQRCode()}
|
||||
onPress={this.saveQRCODE}
|
||||
width="100%"
|
||||
type="secondaryAccented"
|
||||
icon="qrcode"
|
||||
/>
|
||||
<Seperator />
|
||||
<Button
|
||||
onPress={this.saveToTextFile}
|
||||
title={strings.saveAsText()}
|
||||
width="100%"
|
||||
type="secondaryAccented"
|
||||
icon="text"
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
<Button
|
||||
onPress={this.shareFile}
|
||||
title={strings.shareToCloud()}
|
||||
width="100%"
|
||||
type="secondaryAccented"
|
||||
icon="cloud"
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.sm}
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
marginBottom: 5,
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{strings.recoveryKeySavedConfirmation()}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={strings.done()}
|
||||
width="100%"
|
||||
type="error"
|
||||
onPress={this.close}
|
||||
/>
|
||||
</View>
|
||||
</SheetWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RecoveryKeySheet;
|
||||
454
apps/mobile/app/components/sheets/recovery-key/index.tsx
Normal file
454
apps/mobile/app/components/sheets/recovery-key/index.tsx
Normal file
@@ -0,0 +1,454 @@
|
||||
/*
|
||||
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 { sanitizeFilename } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Platform, ScrollView, View } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import Share from "react-native-share";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { db } from "../../../common/database";
|
||||
import filesystem from "../../../common/filesystem";
|
||||
import { presentSheet, ToastManager } from "../../../services/event-manager";
|
||||
import { clearMessage } from "../../../services/message";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import { QRCode } from "../../ui/svg/lazy";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { User } from "@notesnook/core";
|
||||
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
|
||||
type RecoveryKeySheetProps = {
|
||||
close?: (ctx?: string) => void;
|
||||
signup?: boolean;
|
||||
};
|
||||
|
||||
type ActionItem = {
|
||||
id: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
onPress: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
function RecoveryKeySheet({ close }: RecoveryKeySheetProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const [key, setKey] = useState<string | null>(null);
|
||||
const userRef = useRef<User>(undefined);
|
||||
const svgRef = useRef<{
|
||||
toDataURL: (callback: (data: string) => void) => void;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadRecoveryData = async () => {
|
||||
const masterKey = await db.user.getMasterKey();
|
||||
userRef.current = await db.user.getUser();
|
||||
if (masterKey?.key) {
|
||||
setKey(masterKey.key);
|
||||
}
|
||||
};
|
||||
|
||||
void loadRecoveryData();
|
||||
}, []);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
if (!key) return;
|
||||
Clipboard.setString(key);
|
||||
ToastManager.show({
|
||||
heading: strings.recoveryKeyCopied(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
const saveQRCODE = async () => {
|
||||
svgRef.current?.toDataURL(async (data) => {
|
||||
try {
|
||||
let fileName =
|
||||
"nn_" +
|
||||
userRef.current?.email +
|
||||
"_recovery_key_qrcode" +
|
||||
"_" +
|
||||
Date.now();
|
||||
fileName = sanitizeFilename(fileName, { replacement: "_" });
|
||||
fileName = fileName + ".png";
|
||||
|
||||
const path = RNFetchBlob.fs.dirs.CacheDir + fileName;
|
||||
await RNFetchBlob.fs.writeFile(path, data, "base64");
|
||||
await CameraRoll.saveToCameraRoll(`file://` + path);
|
||||
ToastManager.show({
|
||||
heading: strings.recoveryKeyQRCodeSaved(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveToTextFile = async () => {
|
||||
if (!key) return;
|
||||
|
||||
try {
|
||||
let path: string;
|
||||
let fileName = `nn_${userRef.current?.email || "user"}_recovery_key`;
|
||||
fileName = sanitizeFilename(fileName, { replacement: "_" });
|
||||
fileName = `${fileName}.txt`;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const file = await ScopedStorage.createDocument(
|
||||
fileName,
|
||||
"text/plain",
|
||||
key,
|
||||
"utf8"
|
||||
);
|
||||
if (!file) return;
|
||||
path = file.uri;
|
||||
} else {
|
||||
path = await filesystem.checkAndCreateDir("/");
|
||||
await RNFetchBlob.fs.writeFile(path + fileName, key, "utf8");
|
||||
path = path + fileName;
|
||||
}
|
||||
|
||||
ToastManager.show({
|
||||
heading: strings.recoveryKeyTextFileSaved(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
return path;
|
||||
} catch (error) {
|
||||
alert((error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
const shareFile = async () => {
|
||||
const path = await saveToTextFile();
|
||||
if (!path) return;
|
||||
|
||||
try {
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
if (Platform.OS === "ios") {
|
||||
Share.open({
|
||||
url: path,
|
||||
failOnCancel: false
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
} else {
|
||||
FileViewer.open(path, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
//@ts-ignore
|
||||
shareFile: true
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeSheet = () => {
|
||||
close?.();
|
||||
SettingsService.set({ recoveryKeySaved: true });
|
||||
clearMessage();
|
||||
};
|
||||
|
||||
const actionItems: ActionItem[] = [
|
||||
{
|
||||
id: "copy-clipboard",
|
||||
icon: "recovery-key-copy",
|
||||
title: strings.copyToClipboard(),
|
||||
subtitle: "Copy the recovery key to your clipboard",
|
||||
onPress: copyToClipboard
|
||||
},
|
||||
{
|
||||
id: "save-qr",
|
||||
icon: "recovery-key-qr-code",
|
||||
title: strings.saveQRCode(),
|
||||
subtitle: "Save the QR code to your gallery",
|
||||
onPress: saveQRCODE
|
||||
},
|
||||
{
|
||||
id: "save-text",
|
||||
icon: "recovery-key-file",
|
||||
title: strings.saveAsText(),
|
||||
subtitle: "Save the recovery key to a text file",
|
||||
onPress: saveToTextFile
|
||||
},
|
||||
{
|
||||
id: "share-cloud",
|
||||
icon: "recovery-key-cloud-arrow-down",
|
||||
title: strings.shareToCloud(),
|
||||
subtitle: "Securely save the recovery key to cloud",
|
||||
onPress: shareFile
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopLeftRadius: 35,
|
||||
borderTopRightRadius: 35,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_2,
|
||||
maxHeight: "95%"
|
||||
}}
|
||||
>
|
||||
<ScrollView bounces={false} style={{ width: "100%" }}>
|
||||
<View style={{ gap: Spacing.LEVEL_0 }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="recovery-key-key"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
|
||||
<Heading
|
||||
size={AppFontSize.xl}
|
||||
style={{
|
||||
lineHeight: AppFontSize.xl
|
||||
}}
|
||||
fontFamily="SEMI_BOLD"
|
||||
>
|
||||
{strings.saveRecoveryKey()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.primary.paragraph}
|
||||
style={{
|
||||
lineHeight: AppFontSize.xs * 1.2
|
||||
}}
|
||||
>
|
||||
{strings.saveRecoveryKeyDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: colors.primary.border,
|
||||
width: "100%",
|
||||
marginVertical: Spacing.LEVEL_2
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: Radius.S,
|
||||
padding: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="recovery-key-shield-check"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
color={colors.primary.accent}
|
||||
style={{
|
||||
lineHeight: AppFontSize.sm
|
||||
}}
|
||||
>
|
||||
{strings.yourRecoveryKey()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.tertiary.background,
|
||||
borderRadius: Radius.XS,
|
||||
borderWidth: 1.3,
|
||||
borderStyle: "dashed",
|
||||
borderColor: colors.primary.accent,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_3,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
color={colors.primary.paragraph}
|
||||
selectable
|
||||
style={{
|
||||
lineHeight: AppFontSize.sm * 1.2,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{key || ""}
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
icon="recovery-key-copy"
|
||||
iconFamily="notesnook"
|
||||
title={strings.copy()}
|
||||
onPress={copyToClipboard}
|
||||
fontSize={AppFontSize.sm}
|
||||
style={{
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
{actionItems.map((item) => (
|
||||
<Pressable
|
||||
key={item.id}
|
||||
onPress={item.onPress}
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: Radius.S,
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name={item.icon}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ flex: 1, gap: Spacing.LEVEL_0 }}>
|
||||
<Heading size={AppFontSize.sm} fontFamily="MEDIUM">
|
||||
{item.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{item.subtitle}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
title={strings.done()}
|
||||
width="100%"
|
||||
type="accent"
|
||||
onPress={closeSheet}
|
||||
style={{
|
||||
borderRadius: Radius.S
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: -9999,
|
||||
top: -9999,
|
||||
opacity: 0
|
||||
}}
|
||||
>
|
||||
{key ? (
|
||||
<QRCode
|
||||
getRef={(ref) => {
|
||||
svgRef.current = ref;
|
||||
}}
|
||||
size={500}
|
||||
value={key}
|
||||
logoBorderRadius={10}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
RecoveryKeySheet.present = (signup?: boolean) => {
|
||||
presentSheet({
|
||||
disableClosing: false,
|
||||
component: (ref, close) => (
|
||||
<RecoveryKeySheet close={close} signup={signup} />
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
export default RecoveryKeySheet;
|
||||
@@ -19,19 +19,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import {
|
||||
GroupingByIdKey,
|
||||
GroupHeader,
|
||||
GroupingKey,
|
||||
GroupOptions,
|
||||
Item,
|
||||
ItemType,
|
||||
SortOptions
|
||||
SortOptions,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import {
|
||||
getGroupOptions,
|
||||
setGroupOptionsById
|
||||
} from "../../../hooks/use-group-options";
|
||||
import React, { RefObject, useEffect, useRef, useState } from "react";
|
||||
import { FlatList, View } from "react-native";
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { RouteName } from "../../../stores/use-navigation-store";
|
||||
@@ -46,13 +49,20 @@ import { Button } from "../../ui/button";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { useMessageStore } from "../../../stores/use-message-store";
|
||||
|
||||
const Sort = ({
|
||||
dataType,
|
||||
screen,
|
||||
hideGroupOptions,
|
||||
group: groupType,
|
||||
groupId,
|
||||
type
|
||||
type,
|
||||
hideJumpToSection,
|
||||
ref,
|
||||
data
|
||||
}: {
|
||||
dataType: ItemType;
|
||||
type?: GroupingByIdKey;
|
||||
@@ -60,29 +70,42 @@ const Sort = ({
|
||||
group: GroupingKey;
|
||||
hideGroupOptions?: boolean;
|
||||
groupId?: string;
|
||||
hideJumpToSection?: boolean;
|
||||
data?: VirtualizedGrouping<Item>;
|
||||
ref?: RefObject<FlatList>;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [groups, setGroups] = useState<
|
||||
{
|
||||
index: number;
|
||||
group: GroupHeader;
|
||||
}[]
|
||||
>();
|
||||
const offsets = useRef<number[]>([]);
|
||||
const scrollRef = useRef<RefObject<FlatList>>(undefined);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const currentScrollPosition = useRef(0);
|
||||
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
getGroupOptions(groupType, groupId, type)
|
||||
);
|
||||
|
||||
const getSortButtonTitle = () => {
|
||||
const { sortDirection, groupBy, sortBy } = groupOptions || {};
|
||||
const getSortButtonTitle = (type: "asc" | "desc") => {
|
||||
const { groupBy, sortBy } = groupOptions || {};
|
||||
const isAlphabetical = groupBy === "abc" || sortBy === "title";
|
||||
const isDueDate = sortBy === "dueDate";
|
||||
const isRelevance = sortBy === "relevance";
|
||||
|
||||
if (sortDirection === "asc") {
|
||||
if (type === "asc") {
|
||||
if (isAlphabetical) return strings.aToZ();
|
||||
if (isDueDate) return strings.earliestFirst();
|
||||
if (isRelevance) return strings.leastRelevantFirst();
|
||||
return strings.oldNew();
|
||||
return strings.oldestFirst();
|
||||
} else {
|
||||
if (isAlphabetical) return strings.zToA();
|
||||
if (isDueDate) return strings.latestFirst();
|
||||
if (isRelevance) return strings.mostRelevantFirst();
|
||||
return strings.newOld();
|
||||
return strings.newestFirst();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,22 +125,76 @@ const Sort = ({
|
||||
}, 1);
|
||||
};
|
||||
|
||||
const setOrderBy = async () => {
|
||||
const _groupOptions: GroupOptions = {
|
||||
...groupOptions,
|
||||
sortDirection: groupOptions?.sortDirection === "asc" ? "desc" : "asc"
|
||||
};
|
||||
useEffect(() => {
|
||||
data?.groups?.().then((groups) => {
|
||||
setGroups(groups);
|
||||
offsets.current = [];
|
||||
groups.map((item, index) => {
|
||||
let offset = 35 * index;
|
||||
let groupIndex = item.index;
|
||||
const messageState = useMessageStore.getState().message;
|
||||
const msgOffset = messageState?.visible ? 60 : 10;
|
||||
|
||||
await updateGroupOptions(_groupOptions);
|
||||
groupIndex = groupIndex + 1;
|
||||
groupIndex = groupIndex - (index + 1);
|
||||
offset = offset + groupIndex * 100 + msgOffset;
|
||||
offsets.current.push(offset);
|
||||
});
|
||||
|
||||
const index = offsets.current?.findIndex((o, i) => {
|
||||
return (
|
||||
o <= currentScrollPosition.current + 100 &&
|
||||
offsets.current[i + 1] - 100 > currentScrollPosition.current
|
||||
);
|
||||
});
|
||||
|
||||
setCurrentIndex(index < 0 ? 0 : index);
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
const onPress = (item: { index: number; group: GroupHeader }) => {
|
||||
scrollRef.current?.current?.scrollToIndex({
|
||||
index: item.index,
|
||||
animated: true
|
||||
});
|
||||
close();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
data?.groups?.().then((groups) => {
|
||||
setGroups(groups);
|
||||
offsets.current = [];
|
||||
groups.map((item, index) => {
|
||||
let offset = 35 * index;
|
||||
let groupIndex = item.index;
|
||||
const messageState = useMessageStore.getState().message;
|
||||
const msgOffset = messageState?.visible ? 60 : 10;
|
||||
|
||||
groupIndex = groupIndex + 1;
|
||||
groupIndex = groupIndex - (index + 1);
|
||||
offset = offset + groupIndex * 100 + msgOffset;
|
||||
offsets.current.push(offset);
|
||||
});
|
||||
|
||||
const index = offsets.current?.findIndex((o, i) => {
|
||||
return (
|
||||
o <= currentScrollPosition.current + 100 &&
|
||||
offsets.current[i + 1] - 100 > currentScrollPosition.current
|
||||
);
|
||||
});
|
||||
|
||||
setCurrentIndex(index < 0 ? 0 : index);
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
gap: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -128,9 +205,9 @@ const Sort = ({
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.lg}>{strings.sortBy()}</Heading>
|
||||
<Heading fontSize="LG">{strings.sortBy()}</Heading>
|
||||
|
||||
<Button
|
||||
{/* <Button
|
||||
title={getSortButtonTitle()}
|
||||
icon={
|
||||
groupOptions?.sortDirection === "asc"
|
||||
@@ -145,14 +222,16 @@ const Sort = ({
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
onPress={setOrderBy}
|
||||
/>
|
||||
/> */}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
justifyContent: "flex-start",
|
||||
borderBottomColor: colors.primary.border
|
||||
borderBottomColor: colors.primary.border,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{Object.keys(SORT).map((item) => {
|
||||
@@ -178,40 +257,106 @@ const Sort = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
<View
|
||||
key={item}
|
||||
type={groupOptions?.sortBy === item ? "selected" : "plain"}
|
||||
noborder
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
borderRadius: 0,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
onPress={async () => {
|
||||
const _groupOptions: GroupOptions = {
|
||||
...groupOptions,
|
||||
sortBy: item as SortOptions["sortBy"]
|
||||
};
|
||||
await updateGroupOptions(_groupOptions);
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Paragraph>
|
||||
{strings.sortByStrings[
|
||||
item as keyof typeof strings.sortByStrings
|
||||
]()}
|
||||
</Paragraph>
|
||||
<Pressable
|
||||
type={
|
||||
groupOptions?.sortBy === item ? "selected" : "plain-outline"
|
||||
}
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
borderRadius: Radius.XS,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_2
|
||||
}}
|
||||
onPress={async () => {
|
||||
const _groupOptions: GroupOptions = {
|
||||
...groupOptions,
|
||||
sortBy: item as SortOptions["sortBy"]
|
||||
};
|
||||
await updateGroupOptions(_groupOptions);
|
||||
}}
|
||||
>
|
||||
<Paragraph>
|
||||
{strings.sortByStrings[
|
||||
item as keyof typeof strings.sortByStrings
|
||||
]()}
|
||||
</Paragraph>
|
||||
|
||||
{groupOptions?.sortBy === item ? (
|
||||
<AppIcon
|
||||
size={AppFontSize.lg}
|
||||
name="check"
|
||||
color={colors.selected.accent}
|
||||
/>
|
||||
{groupOptions?.sortBy === item ? (
|
||||
<AppIcon
|
||||
size={AppFontSize.lg}
|
||||
name="radio-button"
|
||||
iconFamily="notesnook"
|
||||
color={[colors.selected.accent, "white"]}
|
||||
/>
|
||||
) : null}
|
||||
</Pressable>
|
||||
|
||||
{groupOptions.sortBy === item ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: Radius.S,
|
||||
padding: Spacing.LEVEL_1,
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
borderRadius: Radius.XS,
|
||||
...(groupOptions?.sortDirection === "desc"
|
||||
? getElevationStyle(10)
|
||||
: {})
|
||||
}}
|
||||
type={
|
||||
groupOptions?.sortDirection === "desc"
|
||||
? "accent-background"
|
||||
: "plain"
|
||||
}
|
||||
title={getSortButtonTitle("desc")}
|
||||
onPress={() => {
|
||||
const _groupOptions: GroupOptions = {
|
||||
...groupOptions,
|
||||
sortDirection: "desc"
|
||||
};
|
||||
updateGroupOptions(_groupOptions);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
borderRadius: Radius.XS,
|
||||
...(groupOptions?.sortDirection === "asc"
|
||||
? getElevationStyle(10)
|
||||
: {})
|
||||
}}
|
||||
type={
|
||||
groupOptions?.sortDirection === "asc"
|
||||
? "accent-background"
|
||||
: "plain"
|
||||
}
|
||||
title={getSortButtonTitle("asc")}
|
||||
onPress={() => {
|
||||
const _groupOptions: GroupOptions = {
|
||||
...groupOptions,
|
||||
sortDirection: "asc"
|
||||
};
|
||||
updateGroupOptions(_groupOptions);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
@@ -223,18 +368,19 @@ const Sort = ({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingBottom: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.lg}>{strings.groupBy()}</Heading>
|
||||
<Heading fontSize="LG">{strings.groupBy()}</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap"
|
||||
flexWrap: "wrap",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{Object.keys(GROUP).map((item) => (
|
||||
@@ -243,16 +389,14 @@ const Sort = ({
|
||||
type={
|
||||
groupOptions?.groupBy === GROUP[item as keyof typeof GROUP]
|
||||
? "selected"
|
||||
: "plain"
|
||||
: "plain-outline"
|
||||
}
|
||||
noborder
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
borderRadius: 0,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
width: "auto",
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: Spacing.LEVEL_1
|
||||
}}
|
||||
onPress={async () => {
|
||||
const _groupOptions: GroupOptions = {
|
||||
@@ -262,24 +406,82 @@ const Sort = ({
|
||||
await updateGroupOptions(_groupOptions);
|
||||
}}
|
||||
>
|
||||
<Paragraph>
|
||||
<Paragraph
|
||||
fontFamily={
|
||||
groupOptions?.groupBy === GROUP[item as keyof typeof GROUP]
|
||||
? "SEMI_BOLD"
|
||||
: "REGULAR"
|
||||
}
|
||||
color={
|
||||
groupOptions?.groupBy === GROUP[item as keyof typeof GROUP]
|
||||
? colors.primary.paragraph
|
||||
: colors.secondary.paragraph
|
||||
}
|
||||
>
|
||||
{strings.groupByStrings[
|
||||
item as keyof typeof strings.groupByStrings
|
||||
]()}
|
||||
</Paragraph>
|
||||
|
||||
{groupOptions.groupBy === item ? (
|
||||
<AppIcon
|
||||
size={AppFontSize.lg}
|
||||
name="check"
|
||||
color={colors.selected.accent}
|
||||
/>
|
||||
) : null}
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{!hideJumpToSection && groups ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingBottom: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="LG">{strings.jumpToGroup()}</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{groups?.map((item, index) => {
|
||||
return (
|
||||
<Pressable
|
||||
key={item.group.id}
|
||||
onPress={() => onPress(item)}
|
||||
type={currentIndex === index ? "selected" : "plain-outline"}
|
||||
style={{
|
||||
minWidth: "20%",
|
||||
width: null,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
fontFamily={
|
||||
currentIndex === index ? "SEMI_BOLD" : "REGULAR"
|
||||
}
|
||||
color={colors.primary.paragraph}
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{item.group.title}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
189
apps/mobile/app/components/sheets/trash-interval/index.tsx
Normal file
189
apps/mobile/app/components/sheets/trash-interval/index.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
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 { isFeatureAvailable } from "@notesnook/common";
|
||||
import { TrashCleanupInterval } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { useWindowDimensions, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet, ToastManager } from "../../../services/event-manager";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import PaywallSheet from "../paywall";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
const OPTIONS = [-1, 1, 7, 30, 365] as TrashCleanupInterval[];
|
||||
|
||||
const formatValue = (item: TrashCleanupInterval) => {
|
||||
return item === -1
|
||||
? strings.never()
|
||||
: item === 1
|
||||
? strings.reminderRecurringMode.day()
|
||||
: strings.days(item);
|
||||
};
|
||||
|
||||
type TrashIntervalProps = {
|
||||
close?: (ctx?: string) => void;
|
||||
};
|
||||
|
||||
function TrashInterval({ close }: TrashIntervalProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useWindowDimensions();
|
||||
const [currentValue, setCurrentValue] = useState(
|
||||
db.settings.getTrashCleanupInterval()
|
||||
);
|
||||
|
||||
const onChange = async (item: TrashCleanupInterval) => {
|
||||
const disableTrashFeature = await isFeatureAvailable("disableTrashCleanup");
|
||||
if (!disableTrashFeature.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: disableTrashFeature.error,
|
||||
type: "info",
|
||||
actionText: strings.upgrade(),
|
||||
func: () => {
|
||||
PaywallSheet.present(disableTrashFeature);
|
||||
}
|
||||
});
|
||||
}
|
||||
db.settings.setTrashCleanupInterval(item);
|
||||
setCurrentValue(item);
|
||||
close?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{
|
||||
maxHeight: height * 0.8
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: Spacing.LEVEL_1,
|
||||
paddingTop: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="trash"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.static.red}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.clearTrashInterval()}
|
||||
</Heading>
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{strings.clearTrashIntervalDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopLeftRadius: 35,
|
||||
borderTopRightRadius: 35,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{OPTIONS.map((item) => {
|
||||
const selected = currentValue === item;
|
||||
return (
|
||||
<Pressable
|
||||
key={item}
|
||||
type={selected ? "selected" : "transparent"}
|
||||
onPress={() => onChange(item)}
|
||||
style={{
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: Spacing.LEVEL_2,
|
||||
borderRadius: Radius.XS,
|
||||
borderWidth: selected ? 0 : 1,
|
||||
borderColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="SM"
|
||||
lineHeight="100%"
|
||||
color={colors.primary.heading}
|
||||
>
|
||||
{formatValue(item)}
|
||||
</Heading>
|
||||
|
||||
<AppIcon
|
||||
name={selected ? "radio-button" : "ellipse"}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={
|
||||
selected
|
||||
? [colors.selected.accent, colors.static.white]
|
||||
: colors.secondary.icon
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
TrashInterval.present = () => {
|
||||
presentSheet({
|
||||
component: (_ref, close) => <TrashInterval close={close} />
|
||||
});
|
||||
};
|
||||
|
||||
export default TrashInterval;
|
||||
File diff suppressed because one or more lines are too long
306
apps/mobile/app/components/sheets/update/index.tsx
Normal file
306
apps/mobile/app/components/sheets/update/index.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
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 { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
import { checkVersion, CheckVersionResponse } from "react-native-check-version";
|
||||
import Config from "react-native-config";
|
||||
import deviceInfoModule from "react-native-device-info";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { STORE_LINK } from "../../../utils/constants";
|
||||
import { GithubVersionInfo } from "../../../utils/github-version";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
import { Button } from "../../ui/button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
type UpdateVersion = GithubVersionInfo | CheckVersionResponse;
|
||||
|
||||
type VersionInfo = {
|
||||
version?: string | null;
|
||||
needsUpdate?: boolean;
|
||||
notes?: string;
|
||||
body?: string;
|
||||
};
|
||||
|
||||
type UpdateProps = {
|
||||
version?: UpdateVersion;
|
||||
fwdRef?: RefObject<ActionSheetRef>;
|
||||
};
|
||||
|
||||
type IconTileProps = {
|
||||
colors: ReturnType<typeof useThemeColors>["colors"];
|
||||
name: string;
|
||||
color?: string;
|
||||
backgroundColor?: string;
|
||||
};
|
||||
|
||||
const IconTile = ({ colors, name, color, backgroundColor }: IconTileProps) => (
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: backgroundColor || colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name={name}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={color || colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
export const Update = ({ version: appVersion }: UpdateProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useWindowDimensions();
|
||||
const [version, setVersion] = useState<VersionInfo | undefined>(appVersion);
|
||||
let notes = version?.notes
|
||||
? version.notes.replace("Thank you for using Notesnook!", "").split("- ")
|
||||
: ["Bug fixes and performance improvements"];
|
||||
notes = notes?.map((n) => n.replace(/\n|<br>/g, ""));
|
||||
const isGithubRelease = Config.GITHUB_RELEASE === "true";
|
||||
|
||||
const getSupportedAbi = () => {
|
||||
const abi = deviceInfoModule.supportedAbisSync();
|
||||
const armv8a = abi.find((a) => a === "arm64-v8a");
|
||||
const armv7 = abi.find((a) => a === "armeabi-v7a");
|
||||
|
||||
return armv8a || armv7 || abi[0];
|
||||
};
|
||||
|
||||
const GITHUB_URL =
|
||||
!version || !version.needsUpdate
|
||||
? null
|
||||
: `https://github.com/streetwriters/notesnook/releases/download/${
|
||||
version.version
|
||||
}-android/notesnook-${getSupportedAbi()}.apk`;
|
||||
const GITHUB_PAGE_URL =
|
||||
!version || !version.needsUpdate
|
||||
? null
|
||||
: `https://github.com/streetwriters/notesnook/releases/tag/${version.version}-android`;
|
||||
|
||||
useEffect(() => {
|
||||
if (!version) {
|
||||
(async () => {
|
||||
try {
|
||||
const v = await checkVersion();
|
||||
setVersion(v);
|
||||
} catch (e) {
|
||||
setVersion({
|
||||
needsUpdate: false
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [version]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopLeftRadius: 35,
|
||||
borderTopRightRadius: 35,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{!version || !version?.needsUpdate ? (
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingBottom: Spacing.LEVEL_6
|
||||
}}
|
||||
>
|
||||
{!version ? (
|
||||
<>
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_4
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<Paragraph fontSize="MD" color={colors.secondary.paragraph}>
|
||||
{strings.checkNewVersion()}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconTile colors={colors} name="warning-circle" />
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.noUpdates()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
fontSize="SM"
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{strings.noUpdatesDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_2,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<IconTile colors={colors} name="download-simple" />
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="XL" lineHeight="100%">
|
||||
{strings.updateAvailable()}
|
||||
</Heading>
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{strings.versionReleased(
|
||||
version.version as string,
|
||||
isGithubRelease ? "github" : "store"
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: colors.primary.border
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
nestedScrollEnabled={true}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="MD" lineHeight="100%">
|
||||
{strings.releaseNotes()}
|
||||
</Heading>
|
||||
|
||||
{version.body ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_2,
|
||||
marginBottom: Spacing.LEVEL_0,
|
||||
fontFamily: "monospace",
|
||||
fontSize: 12,
|
||||
lineHeight: 20
|
||||
}}
|
||||
selectable
|
||||
>
|
||||
{version.body}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
{notes.map((item) =>
|
||||
item && item !== "" ? (
|
||||
<Paragraph
|
||||
key={item}
|
||||
color={colors.secondary.paragraph}
|
||||
selectable
|
||||
>
|
||||
{`• ${item}`}
|
||||
</Paragraph>
|
||||
) : null
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
title={
|
||||
isGithubRelease ? strings.downloadUpdate() : strings.update()
|
||||
}
|
||||
onPress={() => {
|
||||
Linking.openURL(
|
||||
(isGithubRelease ? GITHUB_URL : STORE_LINK) as string
|
||||
).catch(console.log);
|
||||
}}
|
||||
type="accent"
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: Radius.S
|
||||
}}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
fontSize="XS"
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(GITHUB_PAGE_URL as string).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}}
|
||||
>
|
||||
{strings.readReleaseNotes[1]()}
|
||||
<Paragraph
|
||||
color={colors.primary.accent}
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="XS"
|
||||
>
|
||||
{strings.readReleaseNotes[2]()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -21,15 +21,14 @@ import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useNetInfo } from "@react-native-community/netinfo";
|
||||
import React from "react";
|
||||
import { ActivityIndicator, Image, Linking, View } from "react-native";
|
||||
import { Image, Linking, View } from "react-native";
|
||||
import { useSheetRef } from "react-native-actions-sheet";
|
||||
import useSyncProgress from "../../../hooks/use-sync-progress";
|
||||
import { presentSheet, ToastManager } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { SyncStatus, useUserStore } from "../../../stores/use-user-store";
|
||||
import { getObfuscatedEmail } from "../../../utils/functions";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { AuthMode } from "../../auth/common";
|
||||
import { Card } from "../../list/card";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
@@ -37,10 +36,12 @@ import { Pressable } from "../../ui/pressable";
|
||||
import { TimeSince } from "../../ui/time-since";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import Sync from "../../../services/sync";
|
||||
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import { logoutUser } from "../../../screens/settings/logout";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
|
||||
export const UserSheet = () => {
|
||||
const ref = useSheetRef();
|
||||
const { colors } = useThemeColors();
|
||||
@@ -57,12 +58,100 @@ export const UserSheet = () => {
|
||||
const { isInternetReachable } = useNetInfo();
|
||||
const isOffline = !isInternetReachable;
|
||||
const { progress } = useSyncProgress();
|
||||
const canShowLastSyncedTime =
|
||||
!!user &&
|
||||
!!lastSynced &&
|
||||
lastSynced !== "Never" &&
|
||||
!syncing &&
|
||||
lastSyncStatus !== SyncStatus.Failed;
|
||||
|
||||
const syncSubtitle = !user
|
||||
? strings.notLoggedIn()
|
||||
: syncing
|
||||
? `${strings.syncing()}${progress ? ` (${progress.current})` : ""}${isOffline ? ` (${strings.offline()})` : ""}`
|
||||
: lastSyncStatus === SyncStatus.Failed
|
||||
? `${strings.syncFailed()}${isOffline ? ` (${strings.offline()})` : ""}`
|
||||
: canShowLastSyncedTime
|
||||
? `Last synced${isOffline ? ` (${strings.offline()})` : ""}`
|
||||
: strings.never();
|
||||
|
||||
const actionItems = [
|
||||
{
|
||||
key: "sync",
|
||||
icon: "user-sheet-sync",
|
||||
title: strings.syncNow(),
|
||||
subtitle: syncSubtitle,
|
||||
onPress: () => {
|
||||
if (!user) return;
|
||||
Sync.run();
|
||||
},
|
||||
hidden: !user
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
icon: "user-sheet-settings",
|
||||
title: strings.settings(),
|
||||
subtitle: "Preferences & app lock",
|
||||
onPress: () => {
|
||||
ref.current?.hide();
|
||||
Navigation.navigate("Settings");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "support",
|
||||
icon: "user-sheet-support",
|
||||
title: strings.emailSupport(),
|
||||
subtitle: "Response within 24 hours",
|
||||
onPress: () => {
|
||||
Clipboard.setString("support@streetwriters.co");
|
||||
ToastManager.show({
|
||||
heading: strings.emailCopied(),
|
||||
type: "success",
|
||||
icon: "content-copy",
|
||||
context: "local"
|
||||
});
|
||||
setTimeout(() => {
|
||||
Linking.openURL("mailto:support@streetwriters.co").catch((e) => {
|
||||
ToastManager.show({
|
||||
message: "Could not open email app",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "documentation",
|
||||
icon: "user-sheet-docs",
|
||||
title: strings.documentation(),
|
||||
subtitle: "Tutorials & help center",
|
||||
onPress: async () => {
|
||||
Linking.openURL("https://docs.notesnook.com");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "logout",
|
||||
icon: "user-sheet-logout",
|
||||
title: strings.logout(),
|
||||
subtitle: "Sign out from this device",
|
||||
onPress: async () => {
|
||||
ref.current?.hide();
|
||||
await sleep(300);
|
||||
logoutUser();
|
||||
},
|
||||
hidden: !user
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center"
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
@@ -70,8 +159,9 @@ export const UserSheet = () => {
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
gap: Spacing.LEVEL_2,
|
||||
width: "100%",
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{userProfile?.profilePicture ? (
|
||||
@@ -80,77 +170,56 @@ export const UserSheet = () => {
|
||||
uri: userProfile?.profilePicture
|
||||
}}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: defaultBorderRadius
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XXL
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XXL,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="account-outline"
|
||||
size={16}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Paragraph size={AppFontSize.xs}>
|
||||
{userProfile?.fullName || getObfuscatedEmail(user.email)}
|
||||
</Paragraph>
|
||||
<View style={{ gap: Spacing.LEVEL_1 }}>
|
||||
<Heading
|
||||
style={{
|
||||
fontSize: AppFontSize.lg,
|
||||
lineHeight: AppFontSize.lg
|
||||
}}
|
||||
fontFamily="MEDIUM"
|
||||
color={colors.primary.heading}
|
||||
>
|
||||
{userProfile?.fullName || strings.account()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
fontSize: AppFontSize.xs,
|
||||
lineHeight: AppFontSize.xs
|
||||
}}
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.heading}
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{!user ? (
|
||||
strings.notLoggedIn()
|
||||
) : lastSynced && lastSynced !== "Never" ? (
|
||||
<>
|
||||
{syncing
|
||||
? `${strings.syncing()} ${
|
||||
progress ? `(${progress.current})` : ""
|
||||
}`
|
||||
: lastSyncStatus === SyncStatus.Failed
|
||||
? strings.syncFailed()
|
||||
: strings.synced()}{" "}
|
||||
{!syncing ? (
|
||||
<TimeSince
|
||||
style={{
|
||||
fontSize: AppFontSize.xxs,
|
||||
color: colors.secondary.paragraph
|
||||
}}
|
||||
updateFrequency={30 * 1000}
|
||||
time={lastSynced as number}
|
||||
/>
|
||||
) : null}
|
||||
{isOffline ? ` (${strings.offline()})` : ""}
|
||||
</>
|
||||
) : (
|
||||
strings.never()
|
||||
)}{" "}
|
||||
<AppIcon
|
||||
name="checkbox-blank-circle"
|
||||
size={10}
|
||||
allowFontScaling
|
||||
color={
|
||||
!user || lastSyncStatus === SyncStatus.Failed
|
||||
? colors.error.icon
|
||||
: isOffline
|
||||
? colors.static.orange
|
||||
: colors.success.icon
|
||||
}
|
||||
/>
|
||||
{getObfuscatedEmail(user.email)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
{syncing ? (
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
size={AppFontSize.xxl}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
@@ -178,171 +247,96 @@ export const UserSheet = () => {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* {user ? (
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_SMALL,
|
||||
gap: DefaultAppStyles.GAP,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xxs}>{strings.storage()}</Paragraph>
|
||||
<Paragraph size={AppFontSize.xxs}>
|
||||
50/100MB {strings.used()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%",
|
||||
height: 5,
|
||||
borderRadius: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.static.black,
|
||||
height: 5,
|
||||
width: "50%",
|
||||
borderRadius: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
borderRadius: 10
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.freePlan()}</Paragraph>
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xxxs}
|
||||
>
|
||||
{strings.viewAllLimits()}
|
||||
<AppIcon name="information" size={AppFontSize.xxxs} />
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title={strings.upgradeNow()}
|
||||
onPress={() => {}}
|
||||
type="accent"
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
|
||||
height: "auto",
|
||||
paddingVertical: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
) : null} */}
|
||||
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
height: 1,
|
||||
width: "100%",
|
||||
borderColor: colors.primary.border,
|
||||
marginVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
height: 1,
|
||||
width: "100%",
|
||||
borderColor: colors.primary.border,
|
||||
marginVertical: 0
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
{[
|
||||
{
|
||||
icon: "reload",
|
||||
title: strings.syncNow(),
|
||||
onPress: () => {
|
||||
Sync.run();
|
||||
},
|
||||
hidden: !user
|
||||
},
|
||||
{
|
||||
icon: "cog-outline",
|
||||
title: strings.settings(),
|
||||
onPress: () => {
|
||||
ref.current?.hide();
|
||||
Navigation.navigate("Settings");
|
||||
}
|
||||
},
|
||||
{
|
||||
title: strings.emailSupport(),
|
||||
icon: "email",
|
||||
onPress: () => {
|
||||
Clipboard.setString("support@streetwriters.co");
|
||||
ToastManager.show({
|
||||
heading: strings.emailCopied(),
|
||||
type: "success",
|
||||
icon: "content-copy"
|
||||
});
|
||||
setTimeout(() => {
|
||||
Linking.openURL("mailto:support@streetwriters.co");
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: strings.documentation(),
|
||||
onPress: async () => {
|
||||
Linking.openURL("https://docs.notesnook.com");
|
||||
},
|
||||
icon: "file-document"
|
||||
},
|
||||
{
|
||||
icon: "logout",
|
||||
title: strings.logout(),
|
||||
onPress: async () => {
|
||||
ref.current?.hide();
|
||||
await sleep(300);
|
||||
logoutUser();
|
||||
},
|
||||
hidden: !user
|
||||
}
|
||||
].map((item) =>
|
||||
<View style={{ gap: Spacing.LEVEL_0 }}>
|
||||
{actionItems.map((item) =>
|
||||
item.hidden ? null : (
|
||||
<Pressable
|
||||
key={item.title}
|
||||
key={item.key}
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
borderRadius: 0,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
onPress={() => {
|
||||
item.onPress();
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.secondary.icon}
|
||||
name={item.icon}
|
||||
size={AppFontSize.xl}
|
||||
/>
|
||||
<Paragraph>{item.title}</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={
|
||||
item.key === "logout"
|
||||
? colors.static.red
|
||||
: colors.primary.icon
|
||||
}
|
||||
iconFamily="notesnook"
|
||||
name={item.icon}
|
||||
size={16}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
|
||||
<Heading
|
||||
style={{
|
||||
fontSize: AppFontSize.md,
|
||||
lineHeight: AppFontSize.md
|
||||
}}
|
||||
fontFamily="SEMI_BOLD"
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
fontSize: AppFontSize.sm,
|
||||
lineHeight: AppFontSize.sm
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{item.key === "sync" && canShowLastSyncedTime ? (
|
||||
<>
|
||||
{item.subtitle}{" "}
|
||||
<TimeSince
|
||||
style={{
|
||||
fontSize: AppFontSize.sm,
|
||||
color: colors.primary.paragraph
|
||||
}}
|
||||
updateFrequency={30 * 1000}
|
||||
time={lastSynced as number}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
item.subtitle
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { useGroupOptions } from "../../hooks/use-group-options";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
@@ -34,9 +33,7 @@ import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { AddNotebookSheet } from "../sheets/add-notebook";
|
||||
import Sort from "../sheets/sort";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
import { SideMenuHome } from "./side-menu-home";
|
||||
import { SideMenuNotebooks } from "./side-menu-notebooks";
|
||||
import { SideMenuTags } from "./side-menu-tags";
|
||||
@@ -44,12 +41,14 @@ import {
|
||||
useSideMenuNotebookSelectionStore,
|
||||
useSideMenuTagsSelectionStore
|
||||
} from "./stores";
|
||||
import { TabBarButton } from "./tab-bar-button";
|
||||
import { useSideBarDraggingStore } from "./dragging-store";
|
||||
import { Button } from "../ui/button";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import PaywallSheet from "../sheets/paywall";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
|
||||
/**
|
||||
* Simple Tab View Implementation for the Side bar
|
||||
@@ -77,7 +76,7 @@ type SimpleTabViewProps = {
|
||||
};
|
||||
|
||||
const createSceneMap = (
|
||||
scenes: Record<string, React.ComponentType<any>>
|
||||
scenes: Record<string, React.ComponentType<unknown>>
|
||||
): ((props: { route: SimpleRoute }) => React.ReactNode) => {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return ({ route }: { route: SimpleRoute }) => {
|
||||
@@ -216,55 +215,61 @@ const TabBar = (props: SimpleTabBarProps) => {
|
||||
const getIcon = (key: string) => {
|
||||
switch (key) {
|
||||
case "home":
|
||||
return "home-outline";
|
||||
return "home";
|
||||
case "notebooks":
|
||||
return "book-outline";
|
||||
return "bookmark";
|
||||
case "tags":
|
||||
return "pound";
|
||||
return "shopping-mode";
|
||||
default:
|
||||
return "home-outline";
|
||||
return "home";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_SMALL,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.primary.border
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
{isSelectionEnabled ? (
|
||||
<>
|
||||
{[
|
||||
{
|
||||
title: "Select all",
|
||||
icon: "check-all"
|
||||
},
|
||||
{
|
||||
title: "Delete",
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
title: "Move",
|
||||
icon: "arrow-right-bold-box-outline",
|
||||
hidden:
|
||||
!notebookSelectionEnabled || props.navigationState.index !== 1
|
||||
},
|
||||
{
|
||||
title: "Close",
|
||||
icon: "close"
|
||||
}
|
||||
].map((item) =>
|
||||
item.hidden ? null : (
|
||||
<>
|
||||
<Pressable
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.primary.border,
|
||||
paddingTop: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{isSelectionEnabled ? (
|
||||
<>
|
||||
{[
|
||||
{
|
||||
title: "Select all",
|
||||
icon: "checks"
|
||||
},
|
||||
{
|
||||
title: "Delete",
|
||||
icon: "trash"
|
||||
},
|
||||
{
|
||||
title: "Move",
|
||||
icon: "drive-file-move",
|
||||
hidden:
|
||||
!notebookSelectionEnabled || props.navigationState.index !== 1
|
||||
},
|
||||
{
|
||||
title: "Close",
|
||||
icon: "close"
|
||||
}
|
||||
].map((item) =>
|
||||
item.hidden ? null : (
|
||||
<TabBarButton
|
||||
key={item.title}
|
||||
icon={item.icon}
|
||||
label={item.title}
|
||||
onPress={async () => {
|
||||
switch (item.title) {
|
||||
case "Select all": {
|
||||
@@ -325,221 +330,182 @@ const TabBar = (props: SimpleTabBarProps) => {
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{dragging ? (
|
||||
<Button
|
||||
onPress={() => {
|
||||
useSideBarDraggingStore.setState({
|
||||
dragging: false
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
type="accent"
|
||||
testID="check"
|
||||
title={strings.done()}
|
||||
icon={"check"}
|
||||
iconSize={AppFontSize.lg - 2}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
paddingVertical: 2,
|
||||
width: "25%"
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
type="plain"
|
||||
>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
color={colors.primary.icon}
|
||||
size={AppFontSize.lg}
|
||||
/>
|
||||
<Paragraph
|
||||
color={colors.primary.paragraph}
|
||||
size={AppFontSize.xxxs - 1}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{dragging ? (
|
||||
<Button
|
||||
onPress={() => {
|
||||
useSideBarDraggingStore.setState({
|
||||
dragging: false
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
type="accent"
|
||||
testID="check"
|
||||
title={strings.done()}
|
||||
icon={"check"}
|
||||
iconSize={AppFontSize.lg - 2}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{props.navigationState.routes.map((route, index) => {
|
||||
const isFocused = props.navigationState.index === index;
|
||||
{props.navigationState.routes.map((route, index) => {
|
||||
const isFocused = props.navigationState.index === index;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={route.key}
|
||||
testID={`tab-${route.key}`}
|
||||
onPress={() => {
|
||||
props.jumpTo(route.key);
|
||||
switch (route.key) {
|
||||
case "notebooks":
|
||||
Navigation.routeNeedsUpdate(
|
||||
"Notebooks",
|
||||
Navigation.routeUpdateFunctions.Notebooks
|
||||
);
|
||||
break;
|
||||
case "tags":
|
||||
Navigation.routeNeedsUpdate(
|
||||
"Tags",
|
||||
Navigation.routeUpdateFunctions.Tags
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
paddingVertical: 2,
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
type={isFocused ? "selected" : "plain"}
|
||||
>
|
||||
<Icon
|
||||
name={getIcon(route.key)}
|
||||
color={
|
||||
isFocused ? colors.selected.icon : colors.primary.icon
|
||||
}
|
||||
size={AppFontSize.lg}
|
||||
return (
|
||||
<TabBarButton
|
||||
key={route.key}
|
||||
testID={`tab-${route.key}`}
|
||||
icon={getIcon(route.key)}
|
||||
label={route.title || ""}
|
||||
isActive={isFocused}
|
||||
onPress={() => {
|
||||
props.jumpTo(route.key);
|
||||
switch (route.key) {
|
||||
case "notebooks":
|
||||
Navigation.routeNeedsUpdate(
|
||||
"Notebooks",
|
||||
Navigation.routeUpdateFunctions.Notebooks
|
||||
);
|
||||
break;
|
||||
case "tags":
|
||||
Navigation.routeNeedsUpdate(
|
||||
"Tags",
|
||||
Navigation.routeUpdateFunctions.Tags
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{props.navigationState.index > 0 ? (
|
||||
<>
|
||||
<IconButton
|
||||
name="plus"
|
||||
testID="sidebar-add-button"
|
||||
size={AppFontSize.lg - 2}
|
||||
top={10}
|
||||
color={colors.primary.icon}
|
||||
onPress={async () => {
|
||||
if (props.navigationState.index === 1) {
|
||||
const notebooksFeature =
|
||||
await isFeatureAvailable("notebooks");
|
||||
if (!notebooksFeature.isAllowed) {
|
||||
PaywallSheet.present(notebooksFeature);
|
||||
return;
|
||||
}
|
||||
|
||||
AddNotebookSheet.present();
|
||||
} else {
|
||||
const tagsFeature = await isFeatureAvailable("tags");
|
||||
if (!tagsFeature.isAllowed) {
|
||||
PaywallSheet.present(tagsFeature);
|
||||
return;
|
||||
}
|
||||
presentDialog({
|
||||
title: strings.addTag(),
|
||||
paragraph: strings.addTagDesc(),
|
||||
input: true,
|
||||
positiveText: "Add",
|
||||
positivePress: async (tag) => {
|
||||
if (tag) {
|
||||
await db.tags.add({
|
||||
title: tag
|
||||
});
|
||||
useTagStore.getState().refresh();
|
||||
return true;
|
||||
}
|
||||
ToastManager.show({
|
||||
context: "local",
|
||||
type: "error",
|
||||
message: strings.allFieldsRequired()
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{props.navigationState.index > 0 ? (
|
||||
<>
|
||||
<TabBarButton
|
||||
icon="plus"
|
||||
testID="sidebar-add-button"
|
||||
label={
|
||||
props.navigationState.index === 1 ? "Notebook" : "Tag"
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: 35,
|
||||
height: 35
|
||||
}}
|
||||
/>
|
||||
onPress={async () => {
|
||||
if (props.navigationState.index === 1) {
|
||||
const notebooksFeature =
|
||||
await isFeatureAvailable("notebooks");
|
||||
if (!notebooksFeature.isAllowed) {
|
||||
PaywallSheet.present(notebooksFeature);
|
||||
return;
|
||||
}
|
||||
|
||||
<IconButton
|
||||
name={
|
||||
groupOptions?.sortDirection === "asc"
|
||||
? "sort-ascending"
|
||||
: "sort-descending"
|
||||
}
|
||||
top={10}
|
||||
testID="sidebar-sort-button"
|
||||
color={colors.primary.icon}
|
||||
onPress={() => {
|
||||
presentSheet({
|
||||
component: (
|
||||
<Sort
|
||||
dataType={
|
||||
props.navigationState.index === 1
|
||||
? "notebook"
|
||||
: "tag"
|
||||
AddNotebookSheet.present();
|
||||
} else {
|
||||
const tagsFeature =
|
||||
await isFeatureAvailable("tags");
|
||||
if (!tagsFeature.isAllowed) {
|
||||
PaywallSheet.present(tagsFeature);
|
||||
return;
|
||||
}
|
||||
presentDialog({
|
||||
title: strings.addTag(),
|
||||
inputLabel: "Enter title",
|
||||
inputPlaceholder: "eg. journal",
|
||||
input: true,
|
||||
positiveText: "Add",
|
||||
positivePress: async (tag) => {
|
||||
if (tag) {
|
||||
await db.tags.add({
|
||||
title: tag
|
||||
});
|
||||
useTagStore.getState().refresh();
|
||||
return true;
|
||||
}
|
||||
ToastManager.show({
|
||||
context: "local",
|
||||
type: "error",
|
||||
message: strings.allFieldsRequired()
|
||||
});
|
||||
return false;
|
||||
}
|
||||
group={
|
||||
props.navigationState.index === 1
|
||||
? "notebooks"
|
||||
: "tags"
|
||||
}
|
||||
hideGroupOptions
|
||||
/>
|
||||
)
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 35,
|
||||
height: 35
|
||||
}}
|
||||
size={AppFontSize.lg - 2}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{props.navigationState.index === 0 ? (
|
||||
<>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
useThemeStore.getState().setColorScheme();
|
||||
}}
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28
|
||||
}}
|
||||
top={10}
|
||||
testID="sidebar-theme-button"
|
||||
color={colors.primary.icon}
|
||||
name={isDark ? "weather-night" : "weather-sunny"}
|
||||
size={AppFontSize.lg - 2}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<TabBarButton
|
||||
icon={
|
||||
groupOptions?.sortDirection === "asc"
|
||||
? "sort-ascending"
|
||||
: "sort-descending"
|
||||
}
|
||||
testID="sidebar-sort-button"
|
||||
label="Sort"
|
||||
onPress={() => {
|
||||
presentSheet({
|
||||
component: (
|
||||
<Sort
|
||||
dataType={
|
||||
props.navigationState.index === 1
|
||||
? "notebook"
|
||||
: "tag"
|
||||
}
|
||||
type={
|
||||
props.navigationState.index === 1
|
||||
? "notebook"
|
||||
: "tag"
|
||||
}
|
||||
group={
|
||||
props.navigationState.index === 1
|
||||
? "notebooks"
|
||||
: "tags"
|
||||
}
|
||||
hideGroupOptions
|
||||
/>
|
||||
)
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{props.navigationState.index === 0 ? (
|
||||
<>
|
||||
<TabBarButton
|
||||
icon={isDark ? "dark-mode-outline" : "sun"}
|
||||
testID="sidebar-theme-button"
|
||||
label={isDark ? "Dark Mode" : "Light Mode"}
|
||||
onPress={() => {
|
||||
useThemeStore.getState().setColorScheme();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,10 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useTotalNotes } from "../../hooks/use-db-item";
|
||||
|
||||
import { db } from "../../common/database";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
subscribeToItemUpdate
|
||||
@@ -32,14 +32,14 @@ import Navigation from "../../services/navigation";
|
||||
import useNavigationStore, {
|
||||
RouteParams
|
||||
} from "../../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import { eAfterSync, eMenuItemUpdate } from "../../utils/events";
|
||||
import { SideMenuItem } from "../../utils/menu-items";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useSideBarDraggingStore } from "./dragging-store";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
|
||||
export function MenuItem({
|
||||
item,
|
||||
@@ -144,36 +144,38 @@ export function MenuItem({
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: Radius.XS,
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
marginBottom: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{renderIcon ? (
|
||||
renderIcon(item, AppFontSize.md)
|
||||
) : (
|
||||
<Icon
|
||||
<AppIcon
|
||||
style={{
|
||||
textAlignVertical: "center",
|
||||
textAlign: "left"
|
||||
}}
|
||||
iconFamily="notesnook"
|
||||
allowFontScaling
|
||||
name={item.icon}
|
||||
color={
|
||||
item.icon === "crown"
|
||||
? colors.static.yellow
|
||||
: isFocused
|
||||
? colors.selected.icon
|
||||
? colors.primary.icon
|
||||
: colors.secondary.icon
|
||||
}
|
||||
size={AppFontSize.md}
|
||||
@@ -184,14 +186,15 @@ export function MenuItem({
|
||||
color={
|
||||
isFocused ? colors.selected.paragraph : colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
fontFamily={isFocused ? "MEDIUM" : "REGULAR"}
|
||||
fontSize="SM"
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
fontSize="XS"
|
||||
color={
|
||||
isFocused ? colors.primary.paragraph : colors.secondary.paragraph
|
||||
}
|
||||
|
||||
@@ -24,19 +24,20 @@ import { StoreApi, UseBoundStore } from "zustand";
|
||||
import { useTotalNotes } from "../../hooks/use-db-item";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastManager
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { TreeItem } from "../../stores/create-notebook-tree-stores";
|
||||
import { SelectionStore } from "../../stores/item-selection-store";
|
||||
import { eOnNotebookUpdated } from "../../utils/events";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { AddNotebookSheet } from "../sheets/add-notebook";
|
||||
|
||||
export const NotebookItem = ({
|
||||
index,
|
||||
@@ -72,7 +73,7 @@ export const NotebookItem = ({
|
||||
const notebook = item.notebook;
|
||||
const isFocused = focused;
|
||||
const { totalNotes, getTotalNotes } = useTotalNotes("notebook");
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
const getTotalNotesRef = React.useRef(getTotalNotes);
|
||||
getTotalNotesRef.current = getTotalNotes;
|
||||
const { colors } = useThemeColors();
|
||||
@@ -94,20 +95,31 @@ export const NotebookItem = ({
|
||||
};
|
||||
}, [item.notebook.id, notebook.id, onItemUpdate]);
|
||||
|
||||
const itemPadding =
|
||||
item.depth === 0 ? undefined : item.depth < 6 ? 15 * item.depth : 15 * 5;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingLeft:
|
||||
item.depth === 0
|
||||
? undefined
|
||||
: item.depth < 6
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
paddingLeft: itemPadding,
|
||||
width: "100%",
|
||||
marginTop: 2,
|
||||
opacity: item.disabled ? 0.5 : 1
|
||||
opacity: item.disabled ? 0.5 : 1,
|
||||
paddingBottom: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
{item.depth > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
width: 1,
|
||||
backgroundColor: colors.primary.border,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
position: "absolute",
|
||||
left: itemPadding
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Pressable
|
||||
type={isFocused || selected ? "selected" : "transparent"}
|
||||
onLongPress={onLongPress}
|
||||
@@ -155,46 +167,38 @@ export const NotebookItem = ({
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
borderRadius: defaultBorderRadius,
|
||||
paddingRight: DefaultAppStyles.GAP_SMALL
|
||||
borderRadius: Radius.XS,
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
marginBottom:
|
||||
expanded && item.hasChildren ? Spacing.LEVEL_0 : undefined
|
||||
// borderLeftWidth: item.depth > 0 ? 1 : undefined,
|
||||
// borderLeftColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size={AppFontSize.md}
|
||||
color={
|
||||
selected || isFocused ? colors.selected.icon : colors.primary.icon
|
||||
}
|
||||
testID={item.hasChildren ? `expand-notebook-${index}` : ""}
|
||||
onPress={() => {
|
||||
if (item.hasChildren && !disableExpand) {
|
||||
onToggleExpanded?.();
|
||||
} else {
|
||||
onPress?.();
|
||||
{item.depth === 0 ? (
|
||||
<AppIcon
|
||||
size={AppFontSize.md}
|
||||
color={
|
||||
selected || isFocused
|
||||
? colors.selected.icon
|
||||
: colors.primary.icon
|
||||
}
|
||||
}}
|
||||
top={0}
|
||||
left={50}
|
||||
bottom={0}
|
||||
right={40}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: defaultBorderRadius
|
||||
}}
|
||||
name={
|
||||
!item.hasChildren || disableExpand
|
||||
? "book-outline"
|
||||
: expanded
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
}
|
||||
/>
|
||||
testID={item.hasChildren ? `expand-notebook-${index}` : ""}
|
||||
style={{
|
||||
borderRadius: defaultBorderRadius
|
||||
}}
|
||||
iconFamily="notesnook"
|
||||
name={"bookmark"}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Paragraph
|
||||
color={
|
||||
@@ -208,7 +212,7 @@ export const NotebookItem = ({
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
gap: Spacing.LEVEL_1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
@@ -217,24 +221,24 @@ export const NotebookItem = ({
|
||||
{selectionEnabled ? (
|
||||
<View
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name={selected ? "checkbox-outline" : "checkbox-blank-outline"}
|
||||
name={selected ? "checkbox" : "box-empty"}
|
||||
iconFamily="notesnook"
|
||||
size={AppFontSize.md}
|
||||
color={selected ? colors.selected.icon : colors.primary.icon}
|
||||
color={
|
||||
selected
|
||||
? [colors.selected.accent, colors.selected.accentForeground]
|
||||
: colors.primary.icon
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{totalNotes?.(notebook?.id) || 0}
|
||||
</Paragraph>
|
||||
</>
|
||||
@@ -260,8 +264,81 @@ export const NotebookItem = ({
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.hasChildren ? (
|
||||
<IconButton
|
||||
size={12}
|
||||
color={
|
||||
selected || isFocused
|
||||
? colors.selected.icon
|
||||
: colors.primary.icon
|
||||
}
|
||||
testID={item.hasChildren ? `expand-notebook-${index}` : ""}
|
||||
onPress={() => {
|
||||
if (item.hasChildren && !disableExpand) {
|
||||
onToggleExpanded?.();
|
||||
}
|
||||
}}
|
||||
top={0}
|
||||
left={20}
|
||||
bottom={0}
|
||||
right={20}
|
||||
style={{
|
||||
borderRadius: defaultBorderRadius,
|
||||
width: undefined,
|
||||
height: undefined
|
||||
}}
|
||||
iconFamily="notesnook"
|
||||
name={expanded ? "chevron-up" : "chevron-down"}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</Pressable>
|
||||
{expanded && item.hasChildren && !selectionEnabled ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingLeft: (item.depth + 1) * 15
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
width: 1,
|
||||
backgroundColor: colors.primary.border,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
position: "absolute",
|
||||
left: (item.depth + 1) * 15
|
||||
}}
|
||||
/>
|
||||
<Pressable
|
||||
style={{
|
||||
// borderLeftWidth: 1,
|
||||
// borderLeftColor: colors.primary.border,
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_1,
|
||||
justifyContent: "flex-start",
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
alignItems: "center"
|
||||
}}
|
||||
onPress={() => {
|
||||
AddNotebookSheet.present(undefined, item.notebook);
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="plus"
|
||||
size={14}
|
||||
style={{
|
||||
marginTop: -2
|
||||
}}
|
||||
iconFamily="notesnook"
|
||||
/>
|
||||
<Heading fontSize="SM">Create sub-notebook</Heading>
|
||||
</Pressable>{" "}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -70,7 +70,7 @@ export const PinnedSection = React.memo(
|
||||
menuPins.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
icon: item.type === "notebook" ? "notebook-outline" : "pound",
|
||||
icon: item.type === "notebook" ? "bookmark" : "shopping-mode",
|
||||
dataType: item.type,
|
||||
data: item,
|
||||
onPress: onPress,
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Pressable } from "../ui/pressable";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { useSideBarDraggingStore } from "./dragging-store";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
|
||||
const SettingsIcon = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -77,31 +78,31 @@ export const SideMenuHeader = (props: { rightButtons?: IconButtonProps[] }) => {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
paddingBottom: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
gap: Spacing.LEVEL_1,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "black",
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 10
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: Radius.XS,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<SvgView width={28} height={28} src={NOTESNOOK_LOGO_SVG} />
|
||||
<SvgView width={26} height={26} src={NOTESNOOK_LOGO_SVG} />
|
||||
</View>
|
||||
|
||||
<Heading size={AppFontSize.lg}>Notesnook</Heading>
|
||||
<Heading lineHeight="120%" fontSize="XL">
|
||||
Notesnook
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
|
||||
@@ -38,6 +38,7 @@ import { ColorSection } from "./color-section";
|
||||
import { MenuItem } from "./menu-item";
|
||||
import { PinnedSection } from "./pinned-section";
|
||||
import { SideMenuHeader } from "./side-menu-header";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
|
||||
const pro = {
|
||||
title: strings.upgradePlan(),
|
||||
@@ -71,8 +72,7 @@ export function SideMenuHome() {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
gap: DefaultAppStyles.GAP,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingTop: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<SideMenuHeader />
|
||||
@@ -129,7 +129,8 @@ export function SideMenuHome() {
|
||||
</>
|
||||
)}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: Spacing.LEVEL_3
|
||||
}}
|
||||
nestedScrollEnabled={false}
|
||||
/>
|
||||
|
||||
@@ -23,9 +23,15 @@ import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { SideMenuHeader } from "./side-menu-header";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
type SideMenuListEmptyProps = {
|
||||
placeholder: string;
|
||||
placeholderTitle: string;
|
||||
placeholderBody: string;
|
||||
placeholderButtonTitle: string;
|
||||
onPressPlaceholderButton: () => void;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
@@ -41,7 +47,7 @@ export const SideMenuListEmpty = (props: SideMenuListEmptyProps) => {
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingTop: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<SideMenuHeader />
|
||||
@@ -76,9 +82,38 @@ export const SideMenuListEmpty = (props: SideMenuListEmptyProps) => {
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{props.placeholder}
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.md} color={colors.primary.heading}>
|
||||
{props.placeholderTitle}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center",
|
||||
maxWidth: "60%"
|
||||
}}
|
||||
fontSize="SM"
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{props.placeholderBody}
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={props.placeholderButtonTitle}
|
||||
onPress={props.onPressPlaceholderButton}
|
||||
fontSize={AppFontSize.sm}
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
paddingHorizontal: Spacing.LEVEL_2
|
||||
}}
|
||||
type="accent"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -41,6 +41,9 @@ import {
|
||||
} from "./stores";
|
||||
import { LegendList } from "@legendapp/list";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import { AddNotebookSheet } from "../sheets/add-notebook";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
useSideMenuNotebookSelectionStore.setState({
|
||||
multiSelect: true
|
||||
});
|
||||
@@ -53,7 +56,7 @@ export const SideMenuNotebooks = () => {
|
||||
const [filteredNotebooks, setFilteredNotebooks] = React.useState(notebooks);
|
||||
const searchTimer = React.useRef<NodeJS.Timeout>(undefined);
|
||||
const lastQuery = React.useRef<string>(undefined);
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
const loadRootNotebooks = React.useCallback(async () => {
|
||||
if (!filteredNotebooks) return;
|
||||
const _notebooks: Notebook[] = [];
|
||||
@@ -81,7 +84,7 @@ export const SideMenuNotebooks = () => {
|
||||
|
||||
useEffect(() => {
|
||||
updateNotebooks();
|
||||
}, [updateNotebooks,updater]);
|
||||
}, [updateNotebooks, updater]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -136,7 +139,12 @@ export const SideMenuNotebooks = () => {
|
||||
>
|
||||
{!notebooks || notebooks.placeholders.length === 0 ? (
|
||||
<SideMenuListEmpty
|
||||
placeholder={strings.emptyPlaceholders("notebook")}
|
||||
placeholderTitle={strings.noNotebooksYet()}
|
||||
placeholderBody={strings.notebooksEmptyBody()}
|
||||
placeholderButtonTitle={strings.createNotebook()}
|
||||
onPressPlaceholderButton={() => {
|
||||
AddNotebookSheet.present();
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
) : (
|
||||
@@ -151,7 +159,8 @@ export const SideMenuNotebooks = () => {
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingTop: Spacing.LEVEL_1,
|
||||
paddingBottom: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<SideMenuHeader />
|
||||
@@ -159,34 +168,44 @@ export const SideMenuNotebooks = () => {
|
||||
}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopColor: colors.primary.border,
|
||||
borderTopWidth: 1,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="Filter notebooks..."
|
||||
<View
|
||||
style={{
|
||||
fontFamily: "Inter-Regular",
|
||||
fontSize: AppFontSize.xs,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopColor: colors.primary.border,
|
||||
borderTopWidth: 1,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
cursorColor={colors.primary.accent}
|
||||
onChangeText={async (value: string) => {
|
||||
searchTimer.current && clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(async () => {
|
||||
lastQuery.current = value;
|
||||
updateNotebooks();
|
||||
}, 500);
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
>
|
||||
<TextInput
|
||||
placeholder={strings.filterNotebooks()}
|
||||
style={{
|
||||
fontFamily: "Inter-Regular",
|
||||
fontSize: AppFontSize.sm,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
}}
|
||||
cursorColor={colors.primary.accent}
|
||||
onChangeText={async (value: string) => {
|
||||
searchTimer.current && clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(async () => {
|
||||
lastQuery.current = value;
|
||||
updateNotebooks();
|
||||
}, 500);
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<AppIcon name="funnel" size={14} iconFamily="notesnook" />
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
@@ -235,8 +254,7 @@ const NotebookItemWrapper = React.memo(
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: index === 0 ? DefaultAppStyles.GAP_VERTICAL : 0
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<NotebookItem
|
||||
|
||||
@@ -25,10 +25,12 @@ import { TextInput, View } from "react-native";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
import { useDBItem, useTotalNotes } from "../../hooks/use-db-item";
|
||||
import { TaggedNotes } from "../../screens/notes/tagged";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useTags } from "../../stores/use-tag-store";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { useTags, useTagStore } from "../../stores/use-tag-store";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Properties } from "../properties";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
@@ -39,6 +41,7 @@ import { SideMenuListEmpty } from "./side-menu-list-empty";
|
||||
import { useSideMenuTagsSelectionStore } from "./stores";
|
||||
import { LegendList, LegendListRenderItemProps } from "@legendapp/list";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
|
||||
const TagItem = (props: {
|
||||
tags: VirtualizedGrouping<Tag>;
|
||||
@@ -56,7 +59,7 @@ const TagItem = (props: {
|
||||
const totalNotes = useTotalNotes("tag");
|
||||
const totalNotesRef = React.useRef(totalNotes);
|
||||
totalNotesRef.current = totalNotes;
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.id) {
|
||||
@@ -67,9 +70,8 @@ const TagItem = (props: {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop:
|
||||
(props.id as number) === 0 ? DefaultAppStyles.GAP_VERTICAL : 2
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
marginBottom: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
{item ? (
|
||||
@@ -116,30 +118,23 @@ const TagItem = (props: {
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
borderRadius: defaultBorderRadius,
|
||||
paddingRight: DefaultAppStyles.GAP_SMALL
|
||||
borderRadius: Radius.XS,
|
||||
padding: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
size={AppFontSize.md}
|
||||
color={isFocused ? colors.selected.icon : colors.primary.icon}
|
||||
name="pound"
|
||||
/>
|
||||
</View>
|
||||
<AppIcon
|
||||
size={16}
|
||||
color={isFocused ? colors.selected.icon : colors.primary.icon}
|
||||
name="shopping-mode"
|
||||
iconFamily="notesnook"
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={
|
||||
@@ -169,9 +164,9 @@ const TagItem = (props: {
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{item?.id && totalNotes.totalNotes?.(item?.id) ? (
|
||||
{item?.id && totalNotes.totalNotes?.(item?.id) !== undefined ? (
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
size={AppFontSize.sm}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{totalNotes.totalNotes(item?.id)}
|
||||
@@ -232,6 +227,32 @@ export const SideMenuTags = () => {
|
||||
setLoading(false);
|
||||
}, [tags]);
|
||||
|
||||
const onPressAddTag = React.useCallback(() => {
|
||||
presentDialog({
|
||||
title: strings.addTag(),
|
||||
// paragraph: strings.addTagDesc(),
|
||||
input: true,
|
||||
inputLabel: "Enter title",
|
||||
inputPlaceholder: "eg. journal",
|
||||
positiveText: strings.add(),
|
||||
positivePress: async (tag) => {
|
||||
if (tag) {
|
||||
await db.tags.add({
|
||||
title: tag
|
||||
});
|
||||
useTagStore.getState().refresh();
|
||||
return true;
|
||||
}
|
||||
ToastManager.show({
|
||||
context: "local",
|
||||
type: "error",
|
||||
message: strings.allFieldsRequired()
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
updateTags();
|
||||
@@ -253,7 +274,10 @@ export const SideMenuTags = () => {
|
||||
>
|
||||
{!tags || tags?.placeholders.length === 0 ? (
|
||||
<SideMenuListEmpty
|
||||
placeholder={strings.emptyPlaceholders("tag")}
|
||||
placeholderTitle={strings.noTagsYet()}
|
||||
placeholderBody={strings.tagsEmptyBody()}
|
||||
placeholderButtonTitle={strings.addTag()}
|
||||
onPressPlaceholderButton={onPressAddTag}
|
||||
isLoading={loading}
|
||||
/>
|
||||
) : (
|
||||
@@ -269,7 +293,8 @@ export const SideMenuTags = () => {
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingTop: Spacing.LEVEL_1,
|
||||
paddingBottom: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<SideMenuHeader />
|
||||
@@ -279,36 +304,45 @@ export const SideMenuTags = () => {
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopColor: colors.primary.border,
|
||||
borderTopWidth: 1,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="Filter tags..."
|
||||
<View
|
||||
style={{
|
||||
fontFamily: "Inter-Regular",
|
||||
fontSize: AppFontSize.xs,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
borderTopColor: colors.primary.border,
|
||||
borderTopWidth: 1,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
cursorColor={colors.primary.accent}
|
||||
onChangeText={async (value) => {
|
||||
searchTimer.current && clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(async () => {
|
||||
try {
|
||||
lastQuery.current = value;
|
||||
updateTags();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
>
|
||||
<TextInput
|
||||
placeholder={strings.filterTags()}
|
||||
style={{
|
||||
fontFamily: "Inter-Regular",
|
||||
fontSize: AppFontSize.sm,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
}}
|
||||
cursorColor={colors.primary.accent}
|
||||
onChangeText={async (value) => {
|
||||
searchTimer.current && clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(async () => {
|
||||
try {
|
||||
lastQuery.current = value;
|
||||
updateTags();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
placeholderTextColor={colors.secondary.placeholder}
|
||||
/>
|
||||
|
||||
<AppIcon name="funnel" size={14} iconFamily="notesnook" />
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
115
apps/mobile/app/components/side-menu/tab-bar-button.tsx
Normal file
115
apps/mobile/app/components/side-menu/tab-bar-button.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withSpring
|
||||
} from "react-native-reanimated";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
|
||||
type TabBarButtonProps = {
|
||||
icon: string;
|
||||
label?: string;
|
||||
onPress: () => void;
|
||||
isActive?: boolean;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
export const TabBarButton = ({
|
||||
icon,
|
||||
label,
|
||||
onPress,
|
||||
isActive = false,
|
||||
testID
|
||||
}: TabBarButtonProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
const animatedIconStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }]
|
||||
}));
|
||||
|
||||
const handlePress = () => {
|
||||
scale.value = withSpring(0.85, {
|
||||
damping: 100,
|
||||
mass: 1,
|
||||
overshootClamping: false
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
scale.value = withSpring(1, {
|
||||
damping: 100,
|
||||
mass: 1,
|
||||
overshootClamping: false
|
||||
});
|
||||
}, 100);
|
||||
|
||||
onPress();
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
testID={testID}
|
||||
onPress={handlePress}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
paddingVertical: 2,
|
||||
width: undefined,
|
||||
gap: label ? Spacing.LEVEL_1 : 0,
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 0
|
||||
}}
|
||||
type={"plain"}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
backgroundColor: isActive ? colors.primary.shade : undefined,
|
||||
borderRadius: Radius.XS,
|
||||
padding: Spacing.LEVEL_1
|
||||
},
|
||||
animatedIconStyle
|
||||
]}
|
||||
>
|
||||
<AppIcon
|
||||
name={icon}
|
||||
iconFamily="notesnook"
|
||||
color={isActive ? colors.primary.icon : colors.secondary.icon}
|
||||
size={16}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
{label && (
|
||||
<Paragraph
|
||||
color={isActive ? colors.primary.heading : colors.secondary.heading}
|
||||
fontFamily={isActive ? "MEDIUM" : "REGULAR"}
|
||||
fontSize={"XS"}
|
||||
>
|
||||
{label}
|
||||
</Paragraph>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
@@ -19,14 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
import { TouchableOpacity, useWindowDimensions, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -37,8 +33,7 @@ import {
|
||||
} from "../../services/event-manager";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eHideToast, eShowToast } from "../../utils/events";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
@@ -130,14 +125,14 @@ export const Toast = ({ context = "global" }) => {
|
||||
...getElevationStyle(5),
|
||||
backgroundColor: isDark ? colors.static.black : colors.static.white,
|
||||
alignSelf: "center",
|
||||
borderRadius: defaultBorderRadius * 2,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderRadius: Radius.MD,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
maxWidth: "90%",
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
gap: Spacing.LEVEL_1,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
@@ -145,7 +140,7 @@ export const Toast = ({ context = "global" }) => {
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
gap: Spacing.LEVEL_1,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -22,6 +22,10 @@ import { ColorValue, TextProps } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import EvilIcon from "react-native-vector-icons/EvilIcons";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { createNanoIconSet } from "react-native-nano-icons";
|
||||
import glyphMap from "../../../../fonts/notesnook-icons.glyphmap.json";
|
||||
|
||||
const NotesnookIcon = createNanoIconSet(glyphMap);
|
||||
|
||||
export interface IconProps extends TextProps {
|
||||
/**
|
||||
@@ -43,9 +47,9 @@ export interface IconProps extends TextProps {
|
||||
* Color of the icon
|
||||
*
|
||||
*/
|
||||
color?: ColorValue | number | undefined;
|
||||
color?: ColorValue | ColorValue[] | number | undefined;
|
||||
|
||||
iconFamily?: "evilicons" | "material";
|
||||
iconFamily?: "evilicons" | "material" | "notesnook";
|
||||
}
|
||||
|
||||
export default function AppIcon({
|
||||
@@ -59,6 +63,12 @@ export default function AppIcon({
|
||||
color={colors.primary.icon}
|
||||
{...(props as any)}
|
||||
/>
|
||||
) : iconFamily === "notesnook" ? (
|
||||
<NotesnookIcon
|
||||
size={AppFontSize.md}
|
||||
color={colors.primary.icon}
|
||||
{...(props as any)}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
size={AppFontSize.md}
|
||||
|
||||
@@ -26,13 +26,14 @@ import {
|
||||
ViewStyle,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import NativeTooltip from "../../../utils/tooltip";
|
||||
import { Pressable, PressableProps, useButton } from "../pressable";
|
||||
import Heading from "../typography/heading";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import { Spacing } from "../../../common/design/spacing";
|
||||
import { FontFamily } from "../../../common/design/font";
|
||||
import AppIcon, { IconProps } from "../AppIcon";
|
||||
export interface ButtonProps extends PressableProps {
|
||||
height?: number;
|
||||
icon?: string;
|
||||
@@ -44,6 +45,7 @@ export interface ButtonProps extends PressableProps {
|
||||
title?: string | null;
|
||||
loading?: boolean;
|
||||
width?: string | number | null;
|
||||
fontFamily?: keyof typeof FontFamily;
|
||||
buttonType?: {
|
||||
text?: ColorValue;
|
||||
selected?: ColorValue;
|
||||
@@ -52,8 +54,9 @@ export interface ButtonProps extends PressableProps {
|
||||
alpha?: number;
|
||||
};
|
||||
bold?: boolean;
|
||||
iconColor?: ColorValue;
|
||||
iconColor?: ColorValue | ColorValue[];
|
||||
iconStyle?: TextStyle;
|
||||
iconFamily?: IconProps["iconFamily"];
|
||||
proTag?: boolean;
|
||||
allowFontScaling?: boolean;
|
||||
}
|
||||
@@ -64,7 +67,8 @@ export const Button = ({
|
||||
loading = false,
|
||||
title = null,
|
||||
icon,
|
||||
fontSize = AppFontSize.sm,
|
||||
fontFamily = "SEMI_BOLD",
|
||||
fontSize = AppFontSize.md,
|
||||
type = "transparent",
|
||||
iconSize = AppFontSize.md,
|
||||
style = {},
|
||||
@@ -75,11 +79,12 @@ export const Button = ({
|
||||
textStyle,
|
||||
iconPosition = "left",
|
||||
buttonType,
|
||||
bold,
|
||||
bold = true,
|
||||
iconColor,
|
||||
fwdRef,
|
||||
proTag,
|
||||
iconStyle,
|
||||
iconFamily,
|
||||
allowFontScaling = true,
|
||||
...restProps
|
||||
}: ButtonProps) => {
|
||||
@@ -118,13 +123,12 @@ export const Button = ({
|
||||
customOpacity={buttonType?.opacity}
|
||||
customAlpha={buttonType?.alpha}
|
||||
style={{
|
||||
// height: typeof height === "number" ? height * growFactor : height,
|
||||
width:
|
||||
typeof width === "number"
|
||||
? width * growFactor
|
||||
: (width as DimensionValue) || undefined,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
borderRadius: defaultBorderRadius,
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
@@ -138,8 +142,9 @@ export const Button = ({
|
||||
<ActivityIndicator color={textColor} size={fontSize + 4} />
|
||||
) : null}
|
||||
{icon && !loading && iconPosition === "left" ? (
|
||||
<Icon
|
||||
<AppIcon
|
||||
name={icon}
|
||||
iconFamily={iconFamily}
|
||||
allowFontScaling={allowFontScaling}
|
||||
style={[{ marginRight: 0 }, iconStyle as any]}
|
||||
color={iconColor || buttonType?.text || textColor}
|
||||
@@ -152,11 +157,18 @@ export const Button = ({
|
||||
color={textColor as string}
|
||||
size={fontSize}
|
||||
numberOfLines={1}
|
||||
fontFamily={fontFamily}
|
||||
allowFontScaling={allowFontScaling}
|
||||
style={[
|
||||
{
|
||||
marginLeft: icon || (loading && iconPosition === "left") ? 5 : 0,
|
||||
marginRight: icon || (loading && iconPosition === "right") ? 5 : 0
|
||||
marginLeft:
|
||||
icon || (loading && iconPosition === "left")
|
||||
? Spacing.LEVEL_1
|
||||
: 0,
|
||||
marginRight:
|
||||
icon || (loading && iconPosition === "right")
|
||||
? Spacing.LEVEL_1
|
||||
: 0
|
||||
},
|
||||
textStyle
|
||||
]}
|
||||
@@ -166,10 +178,11 @@ export const Button = ({
|
||||
)}
|
||||
|
||||
{icon && !loading && iconPosition === "right" ? (
|
||||
<Icon
|
||||
<AppIcon
|
||||
name={icon}
|
||||
iconFamily={iconFamily}
|
||||
allowFontScaling
|
||||
style={[{ marginLeft: 0 }, iconStyle as any]}
|
||||
style={[iconStyle as any]}
|
||||
color={iconColor || buttonType?.text || textColor}
|
||||
size={iconSize}
|
||||
/>
|
||||
|
||||
111
apps/mobile/app/components/ui/circles-background/index.tsx
Normal file
111
apps/mobile/app/components/ui/circles-background/index.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View, ViewStyle } from "react-native";
|
||||
import Svg, { Circle } from "react-native-svg";
|
||||
|
||||
type CirclesBackgroundProps = {
|
||||
/** Width & height of the (square) illustration. */
|
||||
size?: number;
|
||||
/** Centered content, usually an `AppIcon`. */
|
||||
children?: React.ReactNode;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
/**
|
||||
* A reusable concentric-circles illustration with an empty center slot.
|
||||
* Pass any icon/content via `children` to render it centered on the accent
|
||||
* circle. Adapts to light/dark themes.
|
||||
*/
|
||||
export const CirclesBackground = ({
|
||||
size = 100,
|
||||
children,
|
||||
style
|
||||
}: CirclesBackgroundProps) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const accent = colors.primary.accent;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
},
|
||||
style
|
||||
]}
|
||||
>
|
||||
<Svg width={size} height={size} viewBox="0 0 233 233" fill="none">
|
||||
<Circle
|
||||
cx={116.5}
|
||||
cy={116.5}
|
||||
r={104.75}
|
||||
stroke={isDark ? "#1F2722" : "#E8F4ED"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Circle
|
||||
cx={116.5}
|
||||
cy={116.5}
|
||||
r={95.25}
|
||||
fill={accent}
|
||||
fillOpacity={0.05}
|
||||
stroke={isDark ? "#233C2D" : "#E8F0EC"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Circle
|
||||
cx={116.5}
|
||||
cy={116.5}
|
||||
r={82.25}
|
||||
fill={accent}
|
||||
fillOpacity={0.04}
|
||||
stroke={isDark ? "#233C2D" : "#E3F1E8"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Circle
|
||||
cx={116.5}
|
||||
cy={116.5}
|
||||
r={62.25}
|
||||
fill={accent}
|
||||
fillOpacity={0.06}
|
||||
stroke={isDark ? "#233C2D" : "#D3E8DB"}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<Circle cx={116.5} cy={116.5} r={44.5} fill={accent} />
|
||||
</Svg>
|
||||
|
||||
{children ? (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CirclesBackground;
|
||||
@@ -25,10 +25,10 @@ import {
|
||||
TextStyle,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { hexToRGBA, RGB_Linear_Shade } from "../../../utils/colors";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import NativeTooltip from "../../../utils/tooltip";
|
||||
import AppIcon, { IconProps } from "../AppIcon";
|
||||
import { Pressable, PressableProps } from "../pressable";
|
||||
export interface IconButtonProps extends PressableProps {
|
||||
name: string;
|
||||
@@ -42,6 +42,8 @@ export interface IconButtonProps extends PressableProps {
|
||||
tooltipText?: string;
|
||||
tooltipPosition?: number;
|
||||
iconStyle?: TextStyle;
|
||||
iconProps?: IconProps;
|
||||
iconFamily?: IconProps["iconFamily"];
|
||||
}
|
||||
|
||||
export const IconButton = ({
|
||||
@@ -59,7 +61,9 @@ export const IconButton = ({
|
||||
tooltipText,
|
||||
type = "plain",
|
||||
fwdRef,
|
||||
iconProps,
|
||||
tooltipPosition = NativeTooltip.POSITIONS.TOP,
|
||||
iconFamily,
|
||||
...restProps
|
||||
}: IconButtonProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -94,14 +98,16 @@ export const IconButton = ({
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderRadius: 100,
|
||||
width: 40 * growFactor,
|
||||
height: 40 * growFactor,
|
||||
// borderRadius: 100,
|
||||
width: size,
|
||||
height: size,
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
<AppIcon
|
||||
{...iconProps}
|
||||
name={name}
|
||||
iconFamily={iconFamily}
|
||||
style={iconStyle as any}
|
||||
allowFontScaling
|
||||
color={
|
||||
|
||||
@@ -30,8 +30,8 @@ import {
|
||||
} from "react-native";
|
||||
import isEmail from "validator/lib/isEmail";
|
||||
import isURL from "validator/lib/isURL";
|
||||
import { Spacing } from "../../../common/design/spacing";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import AppIcon from "../AppIcon";
|
||||
import { IconButton } from "../icon-button";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
@@ -236,6 +236,7 @@ interface FormInputProps extends TextInputProps {
|
||||
containerStyle?: ViewStyle;
|
||||
wrapperStyle?: ViewStyle;
|
||||
errorStyle?: TextStyle;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function FormInput({
|
||||
@@ -247,7 +248,7 @@ export function FormInput({
|
||||
error,
|
||||
secureTextEntry,
|
||||
customColor,
|
||||
marginBottom = 10,
|
||||
marginBottom = 0,
|
||||
marginRight,
|
||||
button,
|
||||
buttonLeft,
|
||||
@@ -262,6 +263,7 @@ export function FormInput({
|
||||
onPress,
|
||||
onChangeText,
|
||||
errorStyle,
|
||||
label,
|
||||
...restProps
|
||||
}: FormInputProps) {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
@@ -287,31 +289,36 @@ export function FormInput({
|
||||
const fieldError = error || formRef.current.getError(name);
|
||||
|
||||
const borderColor = useMemo(() => {
|
||||
if (fieldError) return colors.error.accent;
|
||||
if (fieldError) return colors.error.border;
|
||||
if (focused) return customColor || colors.selected.border;
|
||||
return colors.primary.border;
|
||||
return "transparent";
|
||||
}, [colors, customColor, fieldError, focused]);
|
||||
|
||||
const style: ViewStyle = {
|
||||
borderWidth: 1,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderColor,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderColor: borderColor,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingRight:
|
||||
buttons || button || secureTextEntry || error
|
||||
? Spacing.LEVEL_3
|
||||
: Spacing.LEVEL_3,
|
||||
...containerStyle
|
||||
};
|
||||
|
||||
const textStyle: TextInputProps["style"] = {
|
||||
paddingHorizontal: 0,
|
||||
fontSize,
|
||||
fontSize: fontSize,
|
||||
color:
|
||||
onPress && loading ? colors.primary.accent : colors.primary.paragraph,
|
||||
paddingTop: Spacing.LEVEL_3,
|
||||
paddingBottom: Spacing.LEVEL_3,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
fontFamily: "Inter-Regular",
|
||||
...(inputStyle as ViewStyle)
|
||||
};
|
||||
@@ -330,6 +337,17 @@ export function FormInput({
|
||||
...wrapperStyle
|
||||
}}
|
||||
>
|
||||
{label ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
fontSize="XS"
|
||||
>
|
||||
{label}
|
||||
</Paragraph>
|
||||
) : undefined}
|
||||
<TouchableOpacity
|
||||
disabled={!loading}
|
||||
onPress={onPress}
|
||||
@@ -368,7 +386,8 @@ export function FormInput({
|
||||
>
|
||||
{secureTextEntry && (
|
||||
<IconButton
|
||||
name="eye"
|
||||
name={!secureEntry ? "eye-closed" : "eye-open"}
|
||||
iconFamily="notesnook"
|
||||
size={20}
|
||||
top={10}
|
||||
bottom={10}
|
||||
@@ -380,9 +399,7 @@ export function FormInput({
|
||||
width: 25,
|
||||
marginLeft: 5
|
||||
}}
|
||||
color={
|
||||
secureEntry ? colors.secondary.icon : colors.primary.accent
|
||||
}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -17,9 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { RefObject, useState } from "react";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import phone from "phone";
|
||||
import React, { RefObject, useRef, useState } from "react";
|
||||
import {
|
||||
ColorValue,
|
||||
findNodeHandle,
|
||||
NativeSyntheticEvent,
|
||||
TextInput,
|
||||
TextInputProps,
|
||||
@@ -28,21 +31,18 @@ import {
|
||||
View,
|
||||
ViewStyle
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import isURL from "validator/lib/isURL";
|
||||
import { Spacing } from "../../../common/design/spacing";
|
||||
import {
|
||||
ERRORS_LIST,
|
||||
validateEmail,
|
||||
validatePass,
|
||||
validateUsername
|
||||
} from "../../../services/validation";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { IconButton } from "../icon-button";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import phone from "phone";
|
||||
import isURL from "validator/lib/isURL";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { useInputError } from "./input-error-context";
|
||||
import { IconProps } from "../AppIcon";
|
||||
|
||||
interface InputProps extends TextInputProps {
|
||||
fwdRef?: RefObject<TextInput | null>;
|
||||
@@ -64,6 +64,7 @@ interface InputProps extends TextInputProps {
|
||||
marginBottom?: number;
|
||||
button?: {
|
||||
icon: string;
|
||||
iconFamily?: IconProps["iconFamily"];
|
||||
color: ColorValue;
|
||||
onPress: () => void;
|
||||
testID?: string;
|
||||
@@ -80,6 +81,7 @@ interface InputProps extends TextInputProps {
|
||||
containerStyle?: ViewStyle;
|
||||
wrapperStyle?: ViewStyle;
|
||||
flexGrow?: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const Input = ({
|
||||
@@ -93,7 +95,7 @@ const Input = ({
|
||||
secureTextEntry,
|
||||
customColor,
|
||||
customValidator,
|
||||
marginBottom = 10,
|
||||
marginBottom = 0,
|
||||
button,
|
||||
onBlurInput,
|
||||
onPress,
|
||||
@@ -103,6 +105,7 @@ const Input = ({
|
||||
buttons,
|
||||
marginRight,
|
||||
buttonLeft,
|
||||
label,
|
||||
flexGrow = 1,
|
||||
inputStyle = {},
|
||||
containerStyle = {},
|
||||
@@ -110,6 +113,9 @@ const Input = ({
|
||||
...restProps
|
||||
}: InputProps) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const errorCtx = useInputError();
|
||||
const internalRef = useRef<TextInput>(null);
|
||||
const activeRef = fwdRef ?? internalRef;
|
||||
const [error, setError] = useState(false);
|
||||
const [focus, setFocus] = useState(false);
|
||||
const [secureEntry, setSecureEntry] = useState(true);
|
||||
@@ -118,10 +124,16 @@ const Input = ({
|
||||
SHORT_PASS: false
|
||||
});
|
||||
type ErrorKey = keyof typeof errorList;
|
||||
|
||||
const reportError = (message: string | null) => {
|
||||
if (!errorCtx) return;
|
||||
const nativeId = findNodeHandle(activeRef.current);
|
||||
if (nativeId !== null) errorCtx.setError(nativeId, message);
|
||||
};
|
||||
const color = error
|
||||
? colors.error.border
|
||||
: focus
|
||||
? customColor || colors.selected.border
|
||||
? customColor || colors.secondary.border
|
||||
: colors.primary.border;
|
||||
|
||||
const validate = async (value: string) => {
|
||||
@@ -134,34 +146,30 @@ const Input = ({
|
||||
});
|
||||
return;
|
||||
}
|
||||
let isError:
|
||||
| boolean
|
||||
| string
|
||||
| { SHORT_PASS?: boolean; isValid?: boolean }
|
||||
| undefined = undefined;
|
||||
let isValid: boolean | string | undefined = undefined;
|
||||
|
||||
switch (validationType) {
|
||||
case "password":
|
||||
isError = validatePass(value);
|
||||
isValid = validatePass(value);
|
||||
break;
|
||||
case "email":
|
||||
isError = validateEmail(value);
|
||||
isValid = validateEmail(value);
|
||||
break;
|
||||
case "username":
|
||||
isError = validateUsername(value);
|
||||
isValid = validateUsername(value);
|
||||
break;
|
||||
case "confirmPassword":
|
||||
isError = customValidator && value === customValidator();
|
||||
isValid = customValidator && value === customValidator();
|
||||
break;
|
||||
case "url":
|
||||
isError = isURL(value, { allow_underscores: true });
|
||||
isValid = isURL(value, { allow_underscores: true });
|
||||
break;
|
||||
case "phonenumber": {
|
||||
const result = phone(value, {
|
||||
strictDetection: true,
|
||||
validateMobilePrefix: true
|
||||
});
|
||||
isError = result.isValid;
|
||||
isValid = result.isValid;
|
||||
if (result.isValid) {
|
||||
onChangeText && onChangeText(result.phoneNumber);
|
||||
}
|
||||
@@ -170,23 +178,10 @@ const Input = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (validationType === "password") {
|
||||
let hasError = false;
|
||||
|
||||
const errors = isError as { [name: string]: boolean };
|
||||
Object.keys(errors).forEach((e) => {
|
||||
//ts-ignore
|
||||
if (errors[e] === true) {
|
||||
hasError = true;
|
||||
}
|
||||
});
|
||||
setError(hasError);
|
||||
onErrorCheck && onErrorCheck(hasError);
|
||||
setErrorList(errors as { SHORT_PASS: boolean });
|
||||
} else {
|
||||
setError(!isError);
|
||||
onErrorCheck && onErrorCheck(!isError);
|
||||
}
|
||||
const hasError = !isValid;
|
||||
setError(hasError);
|
||||
onErrorCheck && onErrorCheck(hasError);
|
||||
reportError(hasError ? (errorMessage ?? null) : null);
|
||||
};
|
||||
|
||||
const onChange = (value: string) => {
|
||||
@@ -198,6 +193,7 @@ const Input = ({
|
||||
setErrorList({
|
||||
SHORT_PASS: false
|
||||
});
|
||||
reportError(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -218,15 +214,12 @@ const Input = ({
|
||||
const style: ViewStyle = {
|
||||
borderWidth: 1,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderColor: color,
|
||||
borderColor: error ? colors.static.red : color,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingRight:
|
||||
buttons || button || secureTextEntry || error
|
||||
? DefaultAppStyles.GAP
|
||||
: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingRight: Spacing.LEVEL_3,
|
||||
...containerStyle
|
||||
};
|
||||
|
||||
@@ -235,10 +228,10 @@ const Input = ({
|
||||
fontSize: fontSize,
|
||||
color:
|
||||
onPress && loading ? colors.primary.accent : colors.primary.paragraph,
|
||||
paddingTop: Spacing.LEVEL_3,
|
||||
paddingBottom: Spacing.LEVEL_3,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
fontFamily: "Inter-Regular",
|
||||
...(inputStyle as ViewStyle)
|
||||
};
|
||||
@@ -253,6 +246,17 @@ const Input = ({
|
||||
...wrapperStyle
|
||||
}}
|
||||
>
|
||||
{label ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
fontSize="XS"
|
||||
>
|
||||
{label}
|
||||
</Paragraph>
|
||||
) : undefined}
|
||||
<TouchableOpacity
|
||||
disabled={!loading}
|
||||
onPress={onPress}
|
||||
@@ -263,7 +267,8 @@ const Input = ({
|
||||
|
||||
<TextInput
|
||||
{...restProps}
|
||||
ref={fwdRef}
|
||||
ref={activeRef}
|
||||
onLayout={restProps.onLayout}
|
||||
editable={!loading && restProps.editable}
|
||||
onChangeText={onChange}
|
||||
onBlur={onBlur}
|
||||
@@ -292,7 +297,8 @@ const Input = ({
|
||||
>
|
||||
{secureTextEntry && (
|
||||
<IconButton
|
||||
name="eye"
|
||||
name={secureEntry ? "eye-closed" : "eye-open"}
|
||||
iconFamily="notesnook"
|
||||
size={20}
|
||||
top={10}
|
||||
bottom={10}
|
||||
@@ -304,9 +310,7 @@ const Input = ({
|
||||
width: 25,
|
||||
marginLeft: 5
|
||||
}}
|
||||
color={
|
||||
secureEntry ? colors.secondary.icon : colors.primary.accent
|
||||
}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -316,97 +320,17 @@ const Input = ({
|
||||
<IconButton
|
||||
testID={button.testID}
|
||||
name={button.icon}
|
||||
size={AppFontSize.xl}
|
||||
iconFamily={button.iconFamily}
|
||||
size={button.size || AppFontSize.xl}
|
||||
top={10}
|
||||
bottom={10}
|
||||
onPress={button.onPress}
|
||||
color={button.color}
|
||||
style={{
|
||||
marginRight: -8
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<IconButton
|
||||
name="alert-circle-outline"
|
||||
top={10}
|
||||
bottom={10}
|
||||
onPress={() => {
|
||||
setShowError(!showError);
|
||||
}}
|
||||
size={20}
|
||||
style={{
|
||||
width: 25,
|
||||
marginLeft: 5
|
||||
}}
|
||||
color={colors.error.icon}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{error && showError && errorMessage ? (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
backgroundColor: colors.secondary.background,
|
||||
paddingVertical: 3,
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL / 2,
|
||||
borderRadius: 2.5,
|
||||
...getElevationStyle(2),
|
||||
top: 0
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{
|
||||
textAlign: "right",
|
||||
textAlignVertical: "bottom"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.xs}
|
||||
color={colors.error.icon}
|
||||
/>{" "}
|
||||
{errorMessage}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{validationType === "password" && focus && (
|
||||
<View
|
||||
style={{
|
||||
marginTop: -5,
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
{Object.keys(errorList).filter(
|
||||
(k) => errorList[k as ErrorKey] === true
|
||||
).length !== 0
|
||||
? Object.keys(ERRORS_LIST).map((error) => (
|
||||
<View
|
||||
key={ERRORS_LIST[error as ErrorKey]}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={errorList[error as ErrorKey] ? "close" : "check"}
|
||||
color={errorList[error as ErrorKey] ? "red" : "green"}
|
||||
/>
|
||||
|
||||
<Paragraph style={{ marginLeft: 5 }} size={AppFontSize.xs}>
|
||||
{ERRORS_LIST[error as ErrorKey]}
|
||||
</Paragraph>
|
||||
</View>
|
||||
))
|
||||
: null}
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
120
apps/mobile/app/components/ui/input/input-error-context.tsx
Normal file
120
apps/mobile/app/components/ui/input/input-error-context.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
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 React, {
|
||||
createContext,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { findNodeHandle, TextInput, View } from "react-native";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { Spacing } from "../../../common/design/spacing";
|
||||
import AppIcon from "../AppIcon";
|
||||
|
||||
interface InputErrorContextType {
|
||||
setError: (nativeId: number, message: string | null) => void;
|
||||
getError: (nativeId: number) => string | null;
|
||||
}
|
||||
|
||||
const InputErrorContext = createContext<InputErrorContextType | null>(null);
|
||||
|
||||
export function InputErrorProvider({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [errors, setErrors] = useState<Map<number, string | null>>(new Map());
|
||||
|
||||
const setError = useCallback((nativeId: number, message: string | null) => {
|
||||
setErrors((prev) => {
|
||||
const next = new Map(prev);
|
||||
if (message === null) {
|
||||
next.delete(nativeId);
|
||||
} else {
|
||||
next.set(nativeId, message);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getError = useCallback(
|
||||
(nativeId: number) => errors.get(nativeId) ?? null,
|
||||
[errors]
|
||||
);
|
||||
|
||||
const value = useMemo(() => ({ setError, getError }), [setError, getError]);
|
||||
|
||||
return (
|
||||
<InputErrorContext.Provider value={value}>
|
||||
{children}
|
||||
</InputErrorContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useInputError() {
|
||||
return useContext(InputErrorContext);
|
||||
}
|
||||
|
||||
interface ErrorContainerProps {
|
||||
inputRef: RefObject<TextInput | null>;
|
||||
}
|
||||
|
||||
export function ErrorContainer({ inputRef }: ErrorContainerProps) {
|
||||
const ctx = useInputError();
|
||||
const { colors } = useThemeColors();
|
||||
const nativeIdRef = useRef<number | null>(null);
|
||||
|
||||
if (!ctx) return null;
|
||||
|
||||
// Resolve the native ID lazily — the ref's current may not be set on first
|
||||
// render but will be by the time the input mounts and registers an error.
|
||||
const nativeId =
|
||||
nativeIdRef.current ??
|
||||
(inputRef.current
|
||||
? (nativeIdRef.current = findNodeHandle(inputRef.current))
|
||||
: null);
|
||||
|
||||
const message = nativeId !== null ? ctx.getError(nativeId) : null;
|
||||
|
||||
if (!message) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="warning-circle"
|
||||
color={colors.static.red}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
/>
|
||||
<Paragraph color={colors.static.red} fontSize="SM">
|
||||
{message}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -20,9 +20,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View, ViewStyle } from "react-native";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { getContainerBorder } from "../../../utils/colors";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import AppIcon from "../AppIcon";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
|
||||
@@ -47,25 +47,40 @@ export const Notice = ({
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
padding: Spacing.LEVEL_2,
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: isSmall ? 5 : 10,
|
||||
backgroundColor:
|
||||
type === "information"
|
||||
? colors.primary.shade
|
||||
: colors.secondary.background,
|
||||
borderRadius: Radius.S,
|
||||
alignItems: "flex-start",
|
||||
gap: 5,
|
||||
gap: Spacing.LEVEL_1,
|
||||
...getContainerBorder(colors.secondary.background),
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
size={isSmall ? AppFontSize.md + 2 : AppFontSize.xxl}
|
||||
name={type}
|
||||
color={type === "alert" ? colors.error.icon : colors.primary.accent}
|
||||
<View
|
||||
style={{
|
||||
marginTop: isSmall ? 3 : 5
|
||||
borderRadius: 100,
|
||||
backgroundColor: colors.primary.accent,
|
||||
width: 20,
|
||||
height: 20,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<AppIcon
|
||||
size={12}
|
||||
name={type === "information" ? "warning-circle" : type}
|
||||
iconFamily="notesnook"
|
||||
color={
|
||||
type === "alert"
|
||||
? colors.error.icon
|
||||
: colors.primary.accentForeground
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1
|
||||
|
||||
351
apps/mobile/app/components/ui/pin-input/index.tsx
Normal file
351
apps/mobile/app/components/ui/pin-input/index.tsx
Normal file
@@ -0,0 +1,351 @@
|
||||
/*
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
AppState,
|
||||
AppStateStatus,
|
||||
KeyboardTypeOptions,
|
||||
TextInput,
|
||||
TextInputKeyPressEvent,
|
||||
TextInputProps,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
|
||||
type PinInputProps = {
|
||||
value: string;
|
||||
length: number;
|
||||
testID?: string;
|
||||
autoFocus?: boolean;
|
||||
inputRef?: RefObject<TextInput | null>;
|
||||
keyboardType?: KeyboardTypeOptions;
|
||||
onSubmitEditing?: TextInputProps["onSubmitEditing"];
|
||||
onChangeText: (value: string) => void;
|
||||
sanitize?: (value: string) => string;
|
||||
showDashAfterDigit?: number;
|
||||
};
|
||||
|
||||
const defaultSanitizer = (value: string) => value;
|
||||
|
||||
const PinInput = ({
|
||||
value,
|
||||
length,
|
||||
testID,
|
||||
autoFocus,
|
||||
inputRef,
|
||||
keyboardType = "number-pad",
|
||||
onSubmitEditing,
|
||||
onChangeText,
|
||||
sanitize = defaultSanitizer,
|
||||
showDashAfterDigit
|
||||
}: PinInputProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const inputRefs = useRef<Array<TextInput | null>>([]);
|
||||
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
||||
const lastAutoFilledValue = useRef<string>("");
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
|
||||
const normalizeToCells = useMemo(
|
||||
() => (raw: string) => {
|
||||
const sanitized = sanitize(raw).slice(0, length);
|
||||
const next = Array.from({ length }, () => "");
|
||||
sanitized.split("").forEach((char, index) => {
|
||||
next[index] = char;
|
||||
});
|
||||
return next;
|
||||
},
|
||||
[length, sanitize]
|
||||
);
|
||||
|
||||
const [cells, setCells] = useState<string[]>(() => normalizeToCells(value));
|
||||
|
||||
const emitChange = React.useCallback(
|
||||
(nextCells: string[]) => {
|
||||
onChangeText(nextCells.join(""));
|
||||
},
|
||||
[onChangeText]
|
||||
);
|
||||
|
||||
const focusCell = React.useCallback(
|
||||
(index: number) => {
|
||||
const clampedIndex = Math.max(0, Math.min(index, length - 1));
|
||||
requestAnimationFrame(() => {
|
||||
inputRefs.current[clampedIndex]?.focus();
|
||||
});
|
||||
},
|
||||
[length]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus) {
|
||||
focusCell(0);
|
||||
}
|
||||
}, [autoFocus, focusCell]);
|
||||
|
||||
useEffect(() => {
|
||||
// Parent-driven resets should clear all slots.
|
||||
if (!value) {
|
||||
setCells(Array.from({ length }, () => ""));
|
||||
return;
|
||||
}
|
||||
|
||||
// Parent-driven complete values (OTP autofill, test paste) should sync.
|
||||
if (value.length >= length) {
|
||||
setCells(normalizeToCells(value));
|
||||
}
|
||||
}, [length, normalizeToCells, value]);
|
||||
|
||||
const onCellChange = (index: number, text: string) => {
|
||||
const sanitized = sanitize(text);
|
||||
const nextCells = [...cells];
|
||||
|
||||
if (!sanitized) {
|
||||
nextCells[index] = "";
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
return;
|
||||
}
|
||||
|
||||
const chars = sanitized.split("");
|
||||
let cursor = index;
|
||||
chars.forEach((char) => {
|
||||
if (cursor < length) {
|
||||
nextCells[cursor] = char;
|
||||
cursor += 1;
|
||||
}
|
||||
});
|
||||
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
|
||||
if (cursor < length) {
|
||||
focusCell(cursor);
|
||||
} else {
|
||||
inputRefs.current[length - 1]?.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const onCellKeyPress = (index: number, event: TextInputKeyPressEvent) => {
|
||||
if (event.nativeEvent.key !== "Backspace") return;
|
||||
|
||||
const nextCells = [...cells];
|
||||
|
||||
if (nextCells[index]) {
|
||||
nextCells[index] = "";
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
nextCells[index - 1] = "";
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
focusCell(index - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const onBulkInputChange = (text: string) => {
|
||||
const sanitized = sanitize(text).slice(0, length);
|
||||
const nextCells = normalizeToCells(sanitized);
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
|
||||
if (sanitized.length < length) {
|
||||
focusCell(sanitized.length);
|
||||
}
|
||||
};
|
||||
|
||||
const applyClipboardCode = React.useCallback(
|
||||
(clipboardText: string) => {
|
||||
const sanitized = sanitize(clipboardText).slice(0, length);
|
||||
if (sanitized.length !== length) return;
|
||||
|
||||
const currentJoined = cells.join("");
|
||||
if (
|
||||
currentJoined === sanitized ||
|
||||
lastAutoFilledValue.current === sanitized
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCells = normalizeToCells(sanitized);
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
lastAutoFilledValue.current = sanitized;
|
||||
inputRefs.current[length - 1]?.blur();
|
||||
},
|
||||
[cells, emitChange, length, normalizeToCells, sanitize]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const readClipboardAndApply = async () => {
|
||||
try {
|
||||
const clipboardText = await Clipboard.getString();
|
||||
if (!clipboardText) return;
|
||||
|
||||
// Prefer not to override a fully entered code while user is interacting.
|
||||
if (cells.join("").length >= length) return;
|
||||
|
||||
applyClipboardCode(clipboardText);
|
||||
} catch {
|
||||
// Ignore clipboard access failures.
|
||||
}
|
||||
};
|
||||
|
||||
const subscription = AppState.addEventListener("change", (nextState) => {
|
||||
const wasInBackground =
|
||||
appStateRef.current === "background" ||
|
||||
appStateRef.current === "inactive";
|
||||
|
||||
if (wasInBackground && nextState === "active") {
|
||||
void readClipboardAndApply();
|
||||
}
|
||||
|
||||
appStateRef.current = nextState;
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, [cells, length, normalizeToCells, sanitize, applyClipboardCode]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexWrap: "nowrap",
|
||||
rowGap: Spacing.LEVEL_1,
|
||||
columnGap: Spacing.LEVEL_1,
|
||||
borderWidth: 1,
|
||||
borderColor:
|
||||
focusedIndex !== null
|
||||
? colors.selected.accent
|
||||
: colors.primary.border,
|
||||
borderRadius: Radius.S,
|
||||
paddingVertical: Spacing.LEVEL_3,
|
||||
paddingHorizontal: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{cells.map((char, index) => {
|
||||
const isActive = focusedIndex === index;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
key={`${index}`}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => focusCell(index)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: isActive
|
||||
? colors.selected.accent
|
||||
: colors.primary.shade,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
ref={(ref) => {
|
||||
inputRefs.current[index] = ref;
|
||||
if (index === 0 && inputRef) {
|
||||
inputRef.current = ref;
|
||||
}
|
||||
}}
|
||||
contextMenuHidden={true}
|
||||
disableFullscreenUI={true}
|
||||
value={char}
|
||||
maxLength={1}
|
||||
keyboardType={keyboardType}
|
||||
onChangeText={(text) => onCellChange(index, text)}
|
||||
onKeyPress={(event) => onCellKeyPress(index, event)}
|
||||
onFocus={() => setFocusedIndex(index)}
|
||||
onBlur={() => {
|
||||
setFocusedIndex((current) =>
|
||||
current === index ? null : current
|
||||
);
|
||||
}}
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
textContentType="oneTimeCode"
|
||||
importantForAutofill="yes"
|
||||
selectionColor={colors.selected.accent}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
textAlign: "center",
|
||||
fontSize: AppFontSize.sm,
|
||||
color: colors.primary.paragraph,
|
||||
padding: 0,
|
||||
margin: 0
|
||||
}}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
blurOnSubmit={index === length - 1}
|
||||
returnKeyType={index === length - 1 ? "done" : "next"}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{showDashAfterDigit === index + 1 ? (
|
||||
<Paragraph>-</Paragraph>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Hidden aggregate input for full-code paste/autofill and test compatibility. */}
|
||||
<TextInput
|
||||
testID={testID}
|
||||
value={cells.join("")}
|
||||
keyboardType={keyboardType}
|
||||
onChangeText={onBulkInputChange}
|
||||
maxLength={length}
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
textContentType="oneTimeCode"
|
||||
importantForAutofill="yes"
|
||||
style={{
|
||||
position: "absolute",
|
||||
opacity: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinInput;
|
||||
@@ -50,10 +50,14 @@ export interface PressableProps extends RNPressableProps {
|
||||
|
||||
type ButtonTypes =
|
||||
| "plain"
|
||||
| "plain-outline"
|
||||
| "accent-background"
|
||||
| "transparent"
|
||||
| "accent"
|
||||
| "shade"
|
||||
| "shade-plain"
|
||||
| "secondary"
|
||||
| "tertiary"
|
||||
| "selectedAccent"
|
||||
| "secondaryAccented"
|
||||
| "inverted"
|
||||
@@ -61,7 +65,10 @@ type ButtonTypes =
|
||||
| "error"
|
||||
| "errorShade"
|
||||
| "warn"
|
||||
| "selected";
|
||||
| "selected"
|
||||
| "accent-outline"
|
||||
| "secondary-outline"
|
||||
| "secondary-simple";
|
||||
|
||||
type ButtonVariant = {
|
||||
primary: string;
|
||||
@@ -83,7 +90,7 @@ const buttonTypes = (
|
||||
} => ({
|
||||
plain: {
|
||||
primary: "transparent",
|
||||
text: colors.primary.paragraph,
|
||||
text: colors.secondary.buttonForeground,
|
||||
selected: colors.primary.hover,
|
||||
borderWidth: 0.8,
|
||||
borderSelectedColor: getColorLinearShade(
|
||||
@@ -92,6 +99,22 @@ const buttonTypes = (
|
||||
isDark
|
||||
)
|
||||
},
|
||||
"plain-outline": {
|
||||
primary: "transparent",
|
||||
text: colors.secondary.buttonForeground,
|
||||
selected: colors.primary.hover,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderSelectedColor: colors.primary.border
|
||||
},
|
||||
"secondary-outline": {
|
||||
primary: "transparent",
|
||||
text: colors.secondary.paragraph,
|
||||
selected: colors.primary.hover,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderSelectedColor: colors.primary.border
|
||||
},
|
||||
transparent: {
|
||||
primary: "transparent",
|
||||
text: colors.primary.accent,
|
||||
@@ -105,7 +128,25 @@ const buttonTypes = (
|
||||
},
|
||||
secondary: {
|
||||
primary: colors.secondary.background,
|
||||
text: colors.secondary.paragraph,
|
||||
text: colors.primary.heading,
|
||||
selected: colors.secondary.background,
|
||||
borderWidth: 0.8,
|
||||
borderColor: getColorLinearShade(colors.secondary.background, 0.05, isDark),
|
||||
borderSelectedColor: getColorLinearShade(
|
||||
colors.selected.background,
|
||||
0.05,
|
||||
isDark
|
||||
)
|
||||
},
|
||||
"secondary-simple": {
|
||||
primary: colors.secondary.background,
|
||||
text: colors.primary.heading,
|
||||
selected: colors.secondary.background,
|
||||
borderWidth: 0
|
||||
},
|
||||
tertiary: {
|
||||
primary: colors.tertiary.background,
|
||||
text: colors.secondary.buttonForeground,
|
||||
selected: colors.secondary.background,
|
||||
borderWidth: 0.8,
|
||||
borderColor: getColorLinearShade(colors.secondary.background, 0.05, isDark),
|
||||
@@ -117,7 +158,7 @@ const buttonTypes = (
|
||||
},
|
||||
selected: {
|
||||
primary: colors.selected.background,
|
||||
text: colors.selected.paragraph,
|
||||
text: colors.selected.buttonForeground,
|
||||
selected: colors.selected.background,
|
||||
borderWidth: 0.8,
|
||||
borderColor: getColorLinearShade(colors.selected.background, 0.05, isDark),
|
||||
@@ -167,6 +208,20 @@ const buttonTypes = (
|
||||
false
|
||||
)
|
||||
},
|
||||
"accent-background": {
|
||||
primary: colors.primary.background,
|
||||
text: colors.primary.accent,
|
||||
selected: colors.primary.background,
|
||||
borderWidth: 0
|
||||
},
|
||||
"accent-outline": {
|
||||
primary: "transparent",
|
||||
text: colors.primary.accent,
|
||||
selected: accent || colors.primary.accent,
|
||||
borderWidth: 1,
|
||||
borderColor: accent || colors.primary.accent,
|
||||
borderSelectedColor: accent || colors.primary.accent
|
||||
},
|
||||
inverted: {
|
||||
primary: colors.primary.background,
|
||||
text: colors.primary.accent,
|
||||
@@ -186,6 +241,15 @@ const buttonTypes = (
|
||||
borderColor: getColorLinearShade(colors.primary.shade, 0.3, isDark),
|
||||
borderSelectedColor: getColorLinearShade(colors.primary.shade, 0.3, isDark)
|
||||
},
|
||||
"shade-plain": {
|
||||
primary: colors.primary.shade,
|
||||
text: colors.primary.heading,
|
||||
selected: colors.primary.accent,
|
||||
colorOpacity: 0.12,
|
||||
borderWidth: 0.8,
|
||||
borderColor: getColorLinearShade(colors.primary.shade, 0.3, isDark),
|
||||
borderSelectedColor: getColorLinearShade(colors.primary.shade, 0.3, isDark)
|
||||
},
|
||||
error: {
|
||||
primary: colors.error.background,
|
||||
text: colors.error.paragraph,
|
||||
@@ -200,7 +264,7 @@ const buttonTypes = (
|
||||
},
|
||||
errorShade: {
|
||||
primary: "transparent",
|
||||
text: colors.error.paragraph,
|
||||
text: colors.error.buttonForeground,
|
||||
selected: colors.error.background,
|
||||
borderWidth: 0.8,
|
||||
borderSelectedColor: getColorLinearShade(
|
||||
|
||||
@@ -19,13 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { isReminderActive } from "@notesnook/core";
|
||||
import React from "react";
|
||||
import { ViewStyle } from "react-native";
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { Button, ButtonProps } from "../button";
|
||||
import { getFormattedReminderTime } from "@notesnook/common";
|
||||
import { Reminder } from "@notesnook/core";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { Radius } from "../../../common/design/spacing";
|
||||
|
||||
export const ReminderTime = ({
|
||||
checkIsActive = true,
|
||||
@@ -55,8 +55,9 @@ export const ReminderTime = ({
|
||||
title={time}
|
||||
key={reminder.id}
|
||||
icon="bell"
|
||||
iconFamily="notesnook"
|
||||
fontSize={AppFontSize.xs}
|
||||
iconSize={AppFontSize.sm}
|
||||
iconSize={12}
|
||||
type="secondary"
|
||||
buttonType={
|
||||
isTodayOrTomorrow
|
||||
@@ -70,7 +71,7 @@ export const ReminderTime = ({
|
||||
}}
|
||||
style={{
|
||||
height: "auto",
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderRadius: Radius.XXS,
|
||||
borderColor: colors.primary.border,
|
||||
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
|
||||
...(style as ViewStyle)
|
||||
|
||||
39
apps/mobile/app/components/ui/seperator/line-separator.tsx
Normal file
39
apps/mobile/app/components/ui/seperator/line-separator.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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 React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { Spacing } from "../../../common/design/spacing";
|
||||
|
||||
const LineSeparator = ({ padding }: { padding: keyof typeof Spacing }) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 1,
|
||||
backgroundColor: colors.primary.border
|
||||
// marginVertical: Spacing[padding]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LineSeparator;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user