mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
mobile: update mobile app settings ui (#9994)
* mobile: update onboarding screen ui * mobile: update ui for paywall flow * mobile: update ui * mobile: update ui * mobile: fix text capitalization * mobile: fix Login screen text capitalization and eye icon mismatch with design * mobile: fix two-factor ui * mobile: ui fixes * mobile: update basic settings ui and reorganize settings screen * mobile: update change-password & recovery-key sheets * mobile: update 2fa sheets design * mobile: add force sync sheets * mobile: replace hard coded strings * mobile: update theme-selector and notesnook circle ui * mobile: update behavior settings * mobile: complete settings module * mobile: fix errors * mobile: settings ui update * mobile: add app-lock password change screens * mobile: add inbox api * mobile: finalize settings ui
This commit is contained in:
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.
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -51,7 +51,7 @@ const DialogButtons = ({
|
||||
{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingHorizontal: Spacing.LEVEL_4
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}
|
||||
]}
|
||||
>
|
||||
@@ -61,7 +61,8 @@ const DialogButtons = ({
|
||||
testID={notesnook.ids.default.dialog.no}
|
||||
type="plain-outline"
|
||||
style={{
|
||||
width: "48.5%"
|
||||
flexGrow: 1,
|
||||
flexShrink: 1
|
||||
}}
|
||||
title={negativeTitle}
|
||||
/>
|
||||
@@ -71,7 +72,8 @@ const DialogButtons = ({
|
||||
fontSize={AppFontSize.md}
|
||||
testID={notesnook.ids.default.dialog.yes}
|
||||
style={{
|
||||
width: "48.5%"
|
||||
flexGrow: 1,
|
||||
flexShrink: 1
|
||||
}}
|
||||
loading={loading}
|
||||
bold
|
||||
|
||||
@@ -47,7 +47,8 @@ const DialogContainer = ({
|
||||
maxHeight: height || 450,
|
||||
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();
|
||||
|
||||
@@ -63,54 +69,83 @@ const DialogHeader = ({
|
||||
>
|
||||
<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" }}
|
||||
fontSize="XL"
|
||||
<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;
|
||||
|
||||
@@ -143,9 +143,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
}, [hide, show]);
|
||||
|
||||
const onNegativePress = async () => {
|
||||
if (dialogInfo?.onClose) {
|
||||
await dialogInfo.onClose();
|
||||
}
|
||||
hide();
|
||||
};
|
||||
|
||||
@@ -218,6 +215,9 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
<DialogHeader
|
||||
title={dialogInfo.title}
|
||||
icon={dialogInfo.icon}
|
||||
iconFamily={dialogInfo.iconFamily}
|
||||
iconType={dialogInfo.iconType}
|
||||
centered={dialogInfo.centered}
|
||||
paragraph={dialogInfo.paragraph}
|
||||
paragraphColor={dialogInfo.paragraphColor}
|
||||
style={{
|
||||
@@ -236,6 +236,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
key={item.name}
|
||||
fwdRef={item.ref}
|
||||
name={item.name}
|
||||
label={item.label}
|
||||
autoFocus={index === 0}
|
||||
placeholder={item.placeholder}
|
||||
formRef={formRef as RefObject<FormRef>}
|
||||
|
||||
@@ -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";
|
||||
@@ -67,9 +66,12 @@ import {
|
||||
} from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import AppIcon from "../../ui/AppIcon";
|
||||
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();
|
||||
@@ -91,7 +93,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);
|
||||
@@ -119,6 +121,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) {
|
||||
@@ -146,7 +149,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;
|
||||
@@ -355,6 +358,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,
|
||||
@@ -638,6 +642,7 @@ export const VaultDialog: React.FC = () => {
|
||||
setIsBiometryAvailable(available);
|
||||
setIsBiometryEnrolled(fingerprint);
|
||||
setBiometricUnlock(fingerprint);
|
||||
setIcon(data.icon);
|
||||
setDeleteAll(false);
|
||||
setLoading(false);
|
||||
|
||||
@@ -707,9 +712,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"
|
||||
}}
|
||||
>
|
||||
@@ -718,13 +723,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"
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
gap: isDeleteVault ? 0 : Spacing.LEVEL_4,
|
||||
marginTop: Spacing.LEVEL_4,
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{(isChangePassword ||
|
||||
@@ -743,20 +756,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();
|
||||
@@ -769,11 +778,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())]}
|
||||
/>
|
||||
|
||||
@@ -800,37 +805,30 @@ export const VaultDialog: React.FC = () => {
|
||||
{isDeleteVault && (
|
||||
<Pressable
|
||||
onPress={() => setDeleteAll(!deleteAll)}
|
||||
icon={deleteAll ? "checkbox" : "box-empty"}
|
||||
iconFamily="notesnook"
|
||||
fontFamily="MEDIUM"
|
||||
iconSize={14}
|
||||
fontSize={AppFontSize.sm}
|
||||
width="100%"
|
||||
style={{
|
||||
paddingVertical: 0,
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
marginTop: isUserLoggedIn
|
||||
? DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
: 0,
|
||||
justifyContent: "flex-start"
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name={
|
||||
deleteAll
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
color={colors.error.accent}
|
||||
size={AppFontSize.md}
|
||||
/>
|
||||
|
||||
<Paragraph color={colors.error.accent} size={AppFontSize.sm}>
|
||||
{strings.deleteAllNotes()}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
title={strings.deleteAllNotes()}
|
||||
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}
|
||||
@@ -843,7 +841,7 @@ export const VaultDialog: React.FC = () => {
|
||||
returnKeyLabel="Change"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
placeholder={strings.newPassword()}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
/>
|
||||
</>
|
||||
@@ -864,12 +862,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"
|
||||
@@ -882,7 +881,7 @@ export const VaultDialog: React.FC = () => {
|
||||
onSubmitEditing={() => {
|
||||
onPress();
|
||||
}}
|
||||
placeholder={strings.confirmPassword()}
|
||||
placeholder={PASSWORD_PLACEHOLDER}
|
||||
validators={[
|
||||
validators.required(strings.confirmPasswordRequired()),
|
||||
validators.matchField(
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 {
|
||||
@@ -44,7 +44,8 @@ export const Header = ({
|
||||
canGoBack,
|
||||
hasSearch,
|
||||
onSearch,
|
||||
rightButton
|
||||
rightButton,
|
||||
style
|
||||
}: {
|
||||
onLeftMenuButtonPress?: () => void;
|
||||
renderedInRoute?: RouteName;
|
||||
@@ -55,6 +56,7 @@ export const Header = ({
|
||||
hasSearch?: boolean;
|
||||
onSearch?: () => void;
|
||||
rightButton?: IconButtonProps;
|
||||
style?: ViewStyle;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [borderHidden, setBorderHidden] = useState(true);
|
||||
@@ -109,15 +111,18 @@ export const Header = ({
|
||||
}}
|
||||
>
|
||||
<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={[
|
||||
{
|
||||
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"
|
||||
>
|
||||
{isTablet && !canGoBack ? null : (
|
||||
@@ -134,7 +139,7 @@ export const Header = ({
|
||||
height: 20
|
||||
}}
|
||||
size={20}
|
||||
name={canGoBack ? "arrow-left" : "menu"}
|
||||
name={canGoBack ? "arrow-back" : "menu"}
|
||||
iconFamily="notesnook"
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
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;
|
||||
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
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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?.analytics?.totalViews > 0 ? (
|
||||
monographMetadata?.analytics.totalViews > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -457,7 +457,7 @@ const PublishNoteSheet = ({
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
|
||||
<Paragraph>
|
||||
{monographMetadata?.analytics?.totalViews || 0}
|
||||
{monographMetadata?.analytics.totalViews || 0}
|
||||
</Paragraph>
|
||||
</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;
|
||||
@@ -160,6 +160,33 @@ const Sort = ({
|
||||
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={{
|
||||
@@ -265,7 +292,7 @@ const Sort = ({
|
||||
{groupOptions?.sortBy === item ? (
|
||||
<AppIcon
|
||||
size={AppFontSize.lg}
|
||||
name="checkbox"
|
||||
name="radio-button"
|
||||
iconFamily="notesnook"
|
||||
color={[colors.selected.accent, "white"]}
|
||||
/>
|
||||
|
||||
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>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -464,6 +464,11 @@ const TabBar = (props: SimpleTabBarProps) => {
|
||||
presentSheet({
|
||||
component: (
|
||||
<Sort
|
||||
dataType={
|
||||
props.navigationState.index === 1
|
||||
? "notebook"
|
||||
: "tag"
|
||||
}
|
||||
type={
|
||||
props.navigationState.index === 1
|
||||
? "notebook"
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
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";
|
||||
@@ -55,7 +54,7 @@ export interface ButtonProps extends PressableProps {
|
||||
alpha?: number;
|
||||
};
|
||||
bold?: boolean;
|
||||
iconColor?: ColorValue;
|
||||
iconColor?: ColorValue | ColorValue[];
|
||||
iconStyle?: TextStyle;
|
||||
iconFamily?: IconProps["iconFamily"];
|
||||
proTag?: boolean;
|
||||
@@ -124,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",
|
||||
|
||||
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;
|
||||
@@ -386,7 +386,7 @@ export function FormInput({
|
||||
>
|
||||
{secureTextEntry && (
|
||||
<IconButton
|
||||
name={secureEntry ? "eye-closed" : "eye-open"}
|
||||
name={!secureEntry ? "eye-closed" : "eye-open"}
|
||||
iconFamily="notesnook"
|
||||
size={20}
|
||||
top={10}
|
||||
|
||||
@@ -42,6 +42,7 @@ import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { IconButton } from "../icon-button";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import { useInputError } from "./input-error-context";
|
||||
import { IconProps } from "../AppIcon";
|
||||
|
||||
interface InputProps extends TextInputProps {
|
||||
fwdRef?: RefObject<TextInput | null>;
|
||||
@@ -63,6 +64,7 @@ interface InputProps extends TextInputProps {
|
||||
marginBottom?: number;
|
||||
button?: {
|
||||
icon: string;
|
||||
iconFamily?: IconProps["iconFamily"];
|
||||
color: ColorValue;
|
||||
onPress: () => void;
|
||||
testID?: string;
|
||||
@@ -217,10 +219,7 @@ const Input = ({
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingRight:
|
||||
buttons || button || secureTextEntry || error
|
||||
? Spacing.LEVEL_3
|
||||
: Spacing.LEVEL_3,
|
||||
paddingRight: Spacing.LEVEL_3,
|
||||
...containerStyle
|
||||
};
|
||||
|
||||
@@ -321,14 +320,12 @@ 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
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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
|
||||
|
||||
@@ -55,6 +55,7 @@ type ButtonTypes =
|
||||
| "transparent"
|
||||
| "accent"
|
||||
| "shade"
|
||||
| "shade-plain"
|
||||
| "secondary"
|
||||
| "tertiary"
|
||||
| "selectedAccent"
|
||||
@@ -65,7 +66,9 @@ type ButtonTypes =
|
||||
| "errorShade"
|
||||
| "warn"
|
||||
| "selected"
|
||||
| "accent-outline";
|
||||
| "accent-outline"
|
||||
| "secondary-outline"
|
||||
| "secondary-simple";
|
||||
|
||||
type ButtonVariant = {
|
||||
primary: string;
|
||||
@@ -87,7 +90,7 @@ const buttonTypes = (
|
||||
} => ({
|
||||
plain: {
|
||||
primary: "transparent",
|
||||
text: colors.primary.buttonForeground,
|
||||
text: colors.secondary.buttonForeground,
|
||||
selected: colors.primary.hover,
|
||||
borderWidth: 0.8,
|
||||
borderSelectedColor: getColorLinearShade(
|
||||
@@ -104,6 +107,14 @@ const buttonTypes = (
|
||||
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,
|
||||
@@ -117,7 +128,7 @@ const buttonTypes = (
|
||||
},
|
||||
secondary: {
|
||||
primary: colors.secondary.background,
|
||||
text: colors.primary.paragraph,
|
||||
text: colors.primary.heading,
|
||||
selected: colors.secondary.background,
|
||||
borderWidth: 0.8,
|
||||
borderColor: getColorLinearShade(colors.secondary.background, 0.05, isDark),
|
||||
@@ -127,6 +138,12 @@ const buttonTypes = (
|
||||
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,
|
||||
@@ -224,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,
|
||||
|
||||
@@ -132,7 +132,10 @@ const SheetWrapper = ({
|
||||
}}
|
||||
indicatorStyle={{
|
||||
width: 100,
|
||||
backgroundColor: colors.secondary.background
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 0,
|
||||
marginTop: 0
|
||||
}}
|
||||
statusBarTranslucent
|
||||
drawUnderStatusBar={true}
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import { DimensionValue, View, ViewStyle } from "react-native";
|
||||
import { SvgXml } from "./lazy";
|
||||
import SvgImage from "react-native-svg/lib/typescript/elements/Image";
|
||||
export const SvgView = ({
|
||||
width = 250,
|
||||
height = 250,
|
||||
|
||||
@@ -63,7 +63,7 @@ import {
|
||||
useTabStore
|
||||
} from "../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController, editorState } from "../screens/editor/tiptap/utils";
|
||||
import { useDragState } from "../screens/settings/editor/state";
|
||||
import { useDragState } from "../screens/settings/components/editor/state";
|
||||
import BackupService from "../services/backup";
|
||||
import BiometricService from "../services/biometrics";
|
||||
import {
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useAreFeaturesAvailable } from "@notesnook/common";
|
||||
import { useEffect } from "react";
|
||||
import { db } from "../common/database";
|
||||
import { useDragState } from "../screens/settings/editor/state";
|
||||
import { useDragState } from "../screens/settings/components/editor/state";
|
||||
import Notifications from "../services/notifications";
|
||||
import SettingsService from "../services/settings";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
|
||||
@@ -48,6 +48,9 @@ export const useVaultStatus = () => {
|
||||
biometryEnrolled: fingerprint,
|
||||
isBiometryAvailable: available ? true : false
|
||||
});
|
||||
VaultStatusDefaults.biometryEnrolled = fingerprint;
|
||||
VaultStatusDefaults.exists = exists;
|
||||
VaultStatusDefaults.isBiometryAvailable = available ? true : false;
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -28,5 +28,5 @@ const EditorMobileSourceUrl =
|
||||
* The url should be something like this: http://192.168.100.126:3000/index.html
|
||||
*/
|
||||
export const EDITOR_URI = __DEV__
|
||||
? "http://192.168.100.144:3000/index.html"
|
||||
? EditorMobileSourceUrl
|
||||
: EditorMobileSourceUrl;
|
||||
|
||||
@@ -80,7 +80,7 @@ import { sleep } from "../../../utils/time";
|
||||
import AddReminder from "../../add-reminder";
|
||||
import ManageTags from "../../manage-tags";
|
||||
import RelationsList from "../../relations-list";
|
||||
import { useDragState } from "../../settings/editor/state";
|
||||
import { useDragState } from "../../settings/components/editor/state";
|
||||
import { EditorMessage, EditorProps, useEditorType } from "./types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { editorState, openInternalLink } from "./utils";
|
||||
|
||||
@@ -1,825 +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, useIsFeatureAvailable } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors, VariantsWithStaticColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import isMobilePhone from "validator/lib/isMobilePhone";
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
Platform,
|
||||
TextInput,
|
||||
View
|
||||
} from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import DialogHeader from "../../components/dialog/dialog-header";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import FormInput, {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../components/ui/input/form-input";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
import Seperator from "../../components/ui/seperator";
|
||||
import { SvgView } from "../../components/ui/svg";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import useTimer from "../../hooks/use-timer";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import PaywallSheet from "../../components/sheets/paywall";
|
||||
const mfaMethods: MFAMethod[] = [
|
||||
{
|
||||
id: "app",
|
||||
title: strings.mfaAuthAppTitle(),
|
||||
body: strings.mfaAuthAppDesc(),
|
||||
icon: "cellphone-key",
|
||||
recommended: true
|
||||
},
|
||||
{
|
||||
id: "sms",
|
||||
title: strings.mfaSmsTitle(),
|
||||
body: strings.mfaSmsDesc(),
|
||||
icon: "message-plus-outline"
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
title: strings.mfaEmailTitle(),
|
||||
body: strings.mfaEmailDesc(),
|
||||
icon: "email-outline"
|
||||
}
|
||||
];
|
||||
type MFAMethod = {
|
||||
id: "email" | "sms" | "app";
|
||||
title?: string;
|
||||
body?: string;
|
||||
icon?: string;
|
||||
recommended?: boolean | undefined;
|
||||
};
|
||||
|
||||
type MFAStep = {
|
||||
id: string;
|
||||
props: { [name: string]: unknown };
|
||||
};
|
||||
|
||||
type MFAStepProps = {
|
||||
recovery?: boolean;
|
||||
onSuccess?: (method?: MFAMethod) => void;
|
||||
setStep?: Dispatch<SetStateAction<MFAStep>>;
|
||||
method?: MFAMethod;
|
||||
isSetup?: boolean;
|
||||
};
|
||||
export const MFAMethodsPickerStep = ({ recovery, onSuccess }: MFAStepProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const featureAvailable = useIsFeatureAvailable("sms2FA");
|
||||
|
||||
const getMethods = () => {
|
||||
if (!recovery) return mfaMethods;
|
||||
return mfaMethods.filter((m) => m.id !== user?.mfa?.primaryMethod);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader
|
||||
title={strings.twoFactorAuth()}
|
||||
paragraph={strings.twoFactorAuthDesc()}
|
||||
padding={12}
|
||||
/>
|
||||
<Seperator />
|
||||
{getMethods().map((item) => (
|
||||
<Pressable
|
||||
key={item.title}
|
||||
onPress={() => {
|
||||
if (
|
||||
item.id === "sms" &&
|
||||
featureAvailable &&
|
||||
!featureAvailable?.isAllowed
|
||||
) {
|
||||
ToastManager.show({
|
||||
message: featureAvailable?.error,
|
||||
type: "info",
|
||||
context: "local",
|
||||
actionText: strings.upgrade(),
|
||||
func: () => {
|
||||
PaywallSheet.present(featureAvailable);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
onSuccess && onSuccess(item);
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 0,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
flexDirection: "row",
|
||||
borderRadius: 0,
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
{item.icon && (
|
||||
<IconButton
|
||||
type="secondary"
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
marginRight: 10
|
||||
}}
|
||||
size={20}
|
||||
color={
|
||||
item.recommended ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
name={item.icon}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.md}>{item.title}</Heading>
|
||||
<Paragraph size={AppFontSize.sm}>{item.body}</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MFASetup = ({
|
||||
method,
|
||||
onSuccess,
|
||||
setStep,
|
||||
recovery
|
||||
}: MFAStepProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const methodId = method?.id;
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
target: "",
|
||||
code: ""
|
||||
})
|
||||
);
|
||||
const targetInputRef = useRef<TextInput>(null);
|
||||
const codeInputRef = useRef<TextInput>(null);
|
||||
const [authenticatorDetails, setAuthenticatorDetails] = useState({
|
||||
sharedKey: null,
|
||||
authenticatorUri: null
|
||||
});
|
||||
const { seconds, setId, start } = useTimer(method?.id);
|
||||
|
||||
const [loading, setLoading] = useState(method?.id === "app" ? true : false);
|
||||
const [enabling, setEnabling] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [generalError, setGeneralError] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (methodId === "app") {
|
||||
setLoading(true);
|
||||
db.mfa
|
||||
?.setup("app")
|
||||
.then((data) => {
|
||||
setAuthenticatorDetails(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
setLoading(false);
|
||||
setGeneralError(error.message);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, [methodId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!methodId) return;
|
||||
|
||||
formRef.current.clearErrors();
|
||||
formRef.current.setValue(
|
||||
"target",
|
||||
methodId === "email"
|
||||
? user?.email || ""
|
||||
: methodId === "app"
|
||||
? authenticatorDetails.sharedKey || ""
|
||||
: formRef.current.getValue("target")
|
||||
);
|
||||
formRef.current.setValue("code", "");
|
||||
setGeneralError(undefined);
|
||||
}, [authenticatorDetails.sharedKey, methodId, user?.email]);
|
||||
|
||||
const codeHelpText = {
|
||||
app: "After putting the above code in authenticator app, the app will display a code that you can enter below.",
|
||||
sms: "You will receive a 2FA code on your phone number which you can enter below",
|
||||
email:
|
||||
"You will receive a 2FA code on your email address which you can enter below"
|
||||
};
|
||||
|
||||
const targetValidators =
|
||||
method?.id === "sms"
|
||||
? [
|
||||
validators.required(strings.phoneNumberNotEntered()),
|
||||
(value: string) =>
|
||||
isMobilePhone(value, "any", {
|
||||
strictMode: true
|
||||
})
|
||||
? undefined
|
||||
: strings.enterValidPhone()
|
||||
]
|
||||
: method?.id === "email"
|
||||
? [
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterValidEmail())
|
||||
]
|
||||
: [];
|
||||
|
||||
const codeValidators = [
|
||||
validators.required(strings.enterSixDigitCode()),
|
||||
(value: string) =>
|
||||
/^\d{6}$/.test(value.trim()) ? undefined : strings.enterSixDigitCode()
|
||||
];
|
||||
|
||||
const onNext = async () => {
|
||||
if (formRef.current.validateField("code")) return;
|
||||
|
||||
try {
|
||||
if (!method) return;
|
||||
const code = formRef.current.getValue("code").trim();
|
||||
|
||||
setGeneralError(undefined);
|
||||
setEnabling(true);
|
||||
if (recovery) {
|
||||
await db.mfa.enableFallback(method.id, code);
|
||||
} else {
|
||||
await db.mfa.enable(method.id, code);
|
||||
}
|
||||
|
||||
const user = await db.user.fetchUser();
|
||||
useUserStore.getState().setUser(user);
|
||||
onSuccess && onSuccess(method);
|
||||
setEnabling(false);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
formRef.current.setError("code", error.message);
|
||||
setEnabling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSendCode = async () => {
|
||||
if (!method || sending) return;
|
||||
|
||||
if (method.id !== "app" && formRef.current.validateField("target")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (method.id === "app" && authenticatorDetails.sharedKey) {
|
||||
Clipboard.setString(authenticatorDetails.sharedKey);
|
||||
if (authenticatorDetails.authenticatorUri) {
|
||||
await Linking.openURL(authenticatorDetails.authenticatorUri).catch(
|
||||
console.log
|
||||
);
|
||||
}
|
||||
|
||||
ToastManager.show({
|
||||
heading: strings.codesCopied(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const target = formRef.current.getValue("target").trim();
|
||||
|
||||
setGeneralError(undefined);
|
||||
if (seconds) {
|
||||
setGeneralError(strings.resendCodeWait());
|
||||
return;
|
||||
}
|
||||
|
||||
setSending(true);
|
||||
await db.mfa.setup(method.id, method.id === "sms" ? target : undefined);
|
||||
|
||||
if (method.id === "sms") {
|
||||
setId(method.id + target);
|
||||
}
|
||||
await sleep(300);
|
||||
start(60, method.id === "sms" ? method.id + target : method.id);
|
||||
setSending(false);
|
||||
ToastManager.show({
|
||||
heading: strings["2faCodeSentVia"](method.id),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
codeInputRef.current?.focus();
|
||||
} catch (e) {
|
||||
setSending(false);
|
||||
const error = e as Error;
|
||||
if (method.id === "sms" || method.id === "email") {
|
||||
formRef.current.setError("target", error.message);
|
||||
} else {
|
||||
setGeneralError(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return !method ? null : (
|
||||
<View>
|
||||
<DialogHeader
|
||||
title={method?.title}
|
||||
paragraph={method?.body}
|
||||
padding={12}
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
marginBottom: 50
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
style={{
|
||||
height: 50
|
||||
}}
|
||||
/>
|
||||
<Paragraph>
|
||||
{strings.gettingInformation()}... {strings.pleaseWait()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<FormInput
|
||||
key={`${method.id}-${authenticatorDetails.sharedKey || user?.email || ""}`}
|
||||
name="target"
|
||||
formRef={formRef}
|
||||
fwdRef={targetInputRef}
|
||||
loading={method?.id !== "sms"}
|
||||
editable={method.id === "sms"}
|
||||
defaultValue={
|
||||
method.id === "email"
|
||||
? user?.email || ""
|
||||
: method.id === "app"
|
||||
? authenticatorDetails.sharedKey || ""
|
||||
: undefined
|
||||
}
|
||||
multiline={method.id === "app"}
|
||||
onChangeText={() => {
|
||||
setGeneralError(undefined);
|
||||
}}
|
||||
placeholder={
|
||||
method.id === "email"
|
||||
? strings.enterEmailAddress()
|
||||
: "+1234567890"
|
||||
}
|
||||
onSubmitEditing={onSendCode}
|
||||
validators={targetValidators}
|
||||
keyboardType={
|
||||
method.id === "email" ? "email-address" : "phone-pad"
|
||||
}
|
||||
buttons={
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
loading={sending}
|
||||
style={{
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
title={
|
||||
sending
|
||||
? null
|
||||
: method.id === "app"
|
||||
? strings.copy()
|
||||
: `${
|
||||
seconds
|
||||
? strings.resendCode(seconds as number)
|
||||
: strings.sendCode()
|
||||
}`
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Heading size={AppFontSize.md}>
|
||||
{strings.enterSixDigitCode()}
|
||||
</Heading>
|
||||
<Paragraph>{codeHelpText[method?.id]}</Paragraph>
|
||||
<Seperator />
|
||||
<FormInput
|
||||
name="code"
|
||||
formRef={formRef}
|
||||
fwdRef={codeInputRef}
|
||||
placeholder="xxxxxx"
|
||||
maxLength={6}
|
||||
loading={loading}
|
||||
textAlign="center"
|
||||
keyboardType="numeric"
|
||||
onChangeText={() => {
|
||||
setGeneralError(undefined);
|
||||
}}
|
||||
onSubmitEditing={onNext}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="done"
|
||||
validators={codeValidators}
|
||||
inputStyle={{
|
||||
fontSize: AppFontSize.lg,
|
||||
height: 60,
|
||||
textAlign: "center",
|
||||
letterSpacing: 10,
|
||||
width: undefined
|
||||
}}
|
||||
containerStyle={{
|
||||
height: 60,
|
||||
borderWidth: 0,
|
||||
width: undefined
|
||||
}}
|
||||
errorStyle={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
/>
|
||||
|
||||
{generalError ? (
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
color: colors.error.icon,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
textAlign: "center",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{generalError}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Seperator />
|
||||
<Button
|
||||
title={enabling ? null : strings.next()}
|
||||
type="accent"
|
||||
width={250}
|
||||
onPress={onNext}
|
||||
loading={enabling}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.change2faMethod()}
|
||||
type="plain"
|
||||
height={25}
|
||||
onPress={() => {
|
||||
setStep &&
|
||||
setStep({
|
||||
id: "mfapick",
|
||||
props: {
|
||||
recovery: recovery
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const MFARecoveryCodes = ({
|
||||
method,
|
||||
onSuccess,
|
||||
isSetup = true
|
||||
}: MFAStepProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [codes, setCodes] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const codes = await db.mfa.codes();
|
||||
if (codes) setCodes(codes);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
ToastManager.error(error, strings.errorGettingCodes(), "local");
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<DialogHeader
|
||||
centered={true}
|
||||
title={strings.saveRecoveryCodes()}
|
||||
paragraph={strings.saveRecoveryCodesDesc()}
|
||||
padding={12}
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
{loading ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
marginBottom: 50
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
style={{
|
||||
height: 50
|
||||
}}
|
||||
/>
|
||||
<Paragraph>
|
||||
{strings.gettingRecoveryCodes()}... {strings.pleaseWait()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<FlatList
|
||||
data={codes}
|
||||
contentContainerStyle={{
|
||||
alignItems: "center"
|
||||
}}
|
||||
numColumns={2}
|
||||
renderItem={({ item }) => (
|
||||
<Heading
|
||||
style={{
|
||||
marginHorizontal: 15,
|
||||
marginVertical: 5,
|
||||
fontFamily: "monospace"
|
||||
}}
|
||||
size={AppFontSize.lg}
|
||||
>
|
||||
{item}
|
||||
</Heading>
|
||||
)}
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={strings.copyCodes()}
|
||||
onPress={() => {
|
||||
const codeString = codes.join("\n");
|
||||
Clipboard.setString(codeString);
|
||||
ToastManager.show({
|
||||
heading: strings.codesCopied(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.saveToFile()}
|
||||
onPress={async () => {
|
||||
try {
|
||||
let path;
|
||||
let fileName = "notesnook_recoverycodes";
|
||||
fileName = sanitizeFilename(fileName, { replacement: "_" });
|
||||
fileName = fileName + ".txt";
|
||||
const codeString = codes.join("\n");
|
||||
if (Platform.OS === "android") {
|
||||
const file = await ScopedStorage.createDocument(
|
||||
fileName,
|
||||
"text/plain",
|
||||
codeString,
|
||||
"utf8"
|
||||
);
|
||||
if (!file) return;
|
||||
path = file.uri;
|
||||
} else {
|
||||
path = await filesystem.checkAndCreateDir("/");
|
||||
await RNFetchBlob.fs.writeFile(
|
||||
path + fileName,
|
||||
codeString,
|
||||
"utf8"
|
||||
);
|
||||
path = path + fileName;
|
||||
}
|
||||
|
||||
ToastManager.show({
|
||||
heading: strings.codesSaved(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
return path;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title={isSetup ? strings.next() : strings.done()}
|
||||
type="accent"
|
||||
width={250}
|
||||
onPress={() => {
|
||||
if (isSetup) {
|
||||
onSuccess && onSuccess(method);
|
||||
} else {
|
||||
eSendEvent(eCloseSheet);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
MFARecoveryCodes.present = (methodId: MFAMethod["id"]) => {
|
||||
presentSheet({
|
||||
component: <MFARecoveryCodes method={{ id: methodId }} isSetup={false} />
|
||||
});
|
||||
};
|
||||
|
||||
const mfaSvg = (
|
||||
colors: VariantsWithStaticColors
|
||||
) => `<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 382.94 405.93">
|
||||
<path fill="${colors.primary.paragraph}" d="M192.58 405.92a75.19 75.19 0 0 1-18.64-2.41l-1.2-.33-1.12-.56c-40.24-20.18-74.19-46.83-100.9-79.21a299.86 299.86 0 0 1-50.95-90.47A348.21 348.21 0 0 1 .07 110.27l.04-2.02c0-20.29 11.26-38.09 28.7-45.35C42.13 57.34 163.24 7.6 172 4c16.48-8.26 34.06-1.36 36.87-.16 6.31 2.58 118.28 48.38 142.47 59.9 24.94 11.87 31.6 33.2 31.6 43.93 0 48.6-8.43 94-25.02 134.97a312.52 312.52 0 0 1-56.16 90.51c-45.85 51.6-91.7 69.89-92.15 70.05a50.11 50.11 0 0 1-17.04 2.72zm-10.79-26.71c3.98.89 13.13 2.22 19.1.05 7.58-2.77 45.96-22.67 81.83-63.03 49.55-55.77 74.7-125.88 74.74-208.38-.1-1.67-1.28-13.59-17.07-21.1-23.72-11.3-140.1-58.89-141.27-59.37l-.32-.14c-2.44-1.02-10.2-3.17-15.55-.37l-1.08.5c-1.3.54-129.86 53.34-143.57 59.05-9.6 4-13 13.9-13 21.83 0 .58-.02 1.43-.05 2.52-1.1 56.44 11.97 195.34 156.24 268.44z"/>
|
||||
<path fill="${colors.secondary.background}" d="M177.33 15.59S47.61 68.87 33.71 74.66c-13.9 5.79-20.85 19.7-20.85 33.6 0 13.9-10.45 195.26 164.47 282.96 0 0 15.88 4.39 27.92 0 12.04-4.39 164.96-78.52 164.96-283.55 0 0 0-20.85-24.33-32.43C321.55 63.66 203.94 15.6 203.94 15.6s-14.44-6.37-26.6 0z"/>
|
||||
<path d="M191.23 57.29v284.25S60.34 278.53 61.51 112.89z" opacity=".2"/>
|
||||
<path fill="${colors.primary.icon}" d="m192.94 261.58-41.69-53.61 24.24-18.86 19.75 25.38 66.7-70.4 22.3 21.13z"/>
|
||||
</svg>`;
|
||||
|
||||
const MFASuccess = ({ recovery }: MFAStepProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Seperator />
|
||||
<SvgView width={150} height={150} src={mfaSvg(colors)} />
|
||||
<Seperator />
|
||||
<DialogHeader
|
||||
centered={true}
|
||||
title={
|
||||
recovery
|
||||
? strings.fallbackMethodEnabled()
|
||||
: strings.twoFactorAuthEnabled()
|
||||
}
|
||||
paragraph={strings.accountIsSecure()}
|
||||
padding={12}
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
<Button
|
||||
title={strings.done()}
|
||||
type="accent"
|
||||
width={250}
|
||||
onPress={() => {
|
||||
eSendEvent(eCloseSheet);
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
/>
|
||||
|
||||
{!recovery ? (
|
||||
<Button
|
||||
title={strings.secondary2faMethod()}
|
||||
type="plain"
|
||||
height={25}
|
||||
onPress={() => {
|
||||
MFASheet.present(true);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const MFASheet = ({ recovery }: { recovery?: boolean }) => {
|
||||
const [step, setStep] = useState<MFAStep>({
|
||||
id: "mfapick",
|
||||
props: {
|
||||
recovery: recovery
|
||||
}
|
||||
});
|
||||
|
||||
const steps: { [name: string]: JSX.Element } = {
|
||||
mfapick: (
|
||||
<MFAMethodsPickerStep
|
||||
recovery={recovery}
|
||||
onSuccess={(method) => {
|
||||
setStep({
|
||||
id: "setup",
|
||||
props: {
|
||||
method: method
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
setup: (
|
||||
<MFASetup
|
||||
recovery={recovery}
|
||||
setStep={setStep}
|
||||
{...step.props}
|
||||
onSuccess={(method) => {
|
||||
setStep({
|
||||
id: "recoveryCodes",
|
||||
props: {
|
||||
method: method
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
recoveryCodes: (
|
||||
<MFARecoveryCodes
|
||||
recovery={recovery}
|
||||
{...step.props}
|
||||
onSuccess={(method) => {
|
||||
setStep({
|
||||
id: "success",
|
||||
props: {
|
||||
method: method
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
success: <MFASuccess {...step.props} recovery={recovery} />
|
||||
};
|
||||
|
||||
return <View>{steps[step.id]}</View>;
|
||||
};
|
||||
|
||||
MFASheet.present = (recovery?: boolean) => {
|
||||
presentSheet({
|
||||
component: <MFASheet recovery={recovery} />
|
||||
});
|
||||
};
|
||||
1024
apps/mobile/app/screens/settings/components/2fa.tsx
Normal file
1024
apps/mobile/app/screens/settings/components/2fa.tsx
Normal file
File diff suppressed because it is too large
Load Diff
118
apps/mobile/app/screens/settings/components/account-card.tsx
Normal file
118
apps/mobile/app/screens/settings/components/account-card.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
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 { Image, View, ViewStyle } from "react-native";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import AppIcon from "../../../components/ui/AppIcon";
|
||||
import Heading from "../../../components/ui/typography/heading";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { getObfuscatedEmail } from "../../../utils/functions";
|
||||
|
||||
type AccountCardProps = {
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
export const AccountCard = ({ style }: AccountCardProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [user, profile] = useUserStore((state) => [state.user, state.profile]);
|
||||
|
||||
const fullName = profile?.fullName || strings.account();
|
||||
const email = user?.email ? getObfuscatedEmail(user.email) : "";
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1,
|
||||
padding: Spacing.LEVEL_2,
|
||||
borderRadius: Radius.S,
|
||||
backgroundColor: colors.primary.shade
|
||||
},
|
||||
style
|
||||
]}
|
||||
>
|
||||
{profile?.profilePicture ? (
|
||||
<Image
|
||||
source={{
|
||||
uri: profile.profilePicture
|
||||
}}
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: Radius.XXL
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: Radius.XXL,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="user"
|
||||
iconFamily="notesnook"
|
||||
size={18}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="LG" fontFamily="SEMI_BOLD" lineHeight="100%">
|
||||
{fullName}
|
||||
</Heading>
|
||||
|
||||
{email ? (
|
||||
<Paragraph
|
||||
fontSize="SM"
|
||||
lineHeight="100%"
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{email}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountCard;
|
||||
@@ -21,16 +21,16 @@ 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 { IconButton } from "../../components/ui/icon-button";
|
||||
import { ProgressBarComponent } from "../../components/ui/svg/lazy";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { db } from "../../../common/database";
|
||||
import { IconButton } from "../../../components/ui/icon-button";
|
||||
import { ProgressBarComponent } from "../../../components/ui/svg/lazy";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
|
||||
import { useDBItem } from "../../../hooks/use-db-item";
|
||||
import { useAttachmentStore } from "../../../stores/use-attachment-store";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
export const AttachmentGroupProgress = (props: { groupId?: string }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -19,30 +19,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { ReactElement } from "react";
|
||||
import { View } from "react-native";
|
||||
import { AttachmentDialog } from "../../components/attachments";
|
||||
import { ChangePassword } from "../../components/auth/change-password";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { AttachmentDialog } from "../../../components/attachments";
|
||||
import { ChangePassword } from "../../../components/auth/change-password";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { AttachmentGroupProgress } from "./attachment-group-progress";
|
||||
import { ChangeEmail } from "./change-email";
|
||||
import DebugLogs from "./debug";
|
||||
import { ConfigureToolbar } from "./editor/configure-toolbar";
|
||||
import { Licenses } from "./licenses";
|
||||
import {
|
||||
ApplockTimerPicker,
|
||||
BackupReminderPicker,
|
||||
BackupWithAttachmentsReminderPicker,
|
||||
DateFormatPicker,
|
||||
DayFormatPicker,
|
||||
WeekFormatPicker,
|
||||
FontPicker,
|
||||
HomePicker,
|
||||
ImageCompressionPicker,
|
||||
SidebarTabPicker,
|
||||
TimeFormatPicker,
|
||||
TrashIntervalPicker,
|
||||
VaultLockTimerPicker
|
||||
TimeFormatPicker
|
||||
} from "./picker/pickers";
|
||||
import { RestoreBackup } from "./restore-backup";
|
||||
import { RestoreBackup } from "../restore-backup";
|
||||
import { ServersConfiguration } from "./server-config";
|
||||
import SoundPicker from "./sound-picker";
|
||||
import ThemeSelector from "./theme-selector";
|
||||
@@ -52,8 +48,9 @@ import {
|
||||
ManageInboxKeys,
|
||||
InboxKeysList,
|
||||
SetupInboxKeys
|
||||
} from "./manage-inbox-keys";
|
||||
import { FailedInboxItems } from "./failed-inbox-items";
|
||||
} from "../manage-inbox-keys";
|
||||
import { FailedInboxItems } from "../failed-inbox-items";
|
||||
import AccountCard from "./account-card";
|
||||
|
||||
export const components: { [name: string]: ReactElement } = {
|
||||
homeselector: <HomePicker />,
|
||||
@@ -62,17 +59,13 @@ export const components: { [name: string]: ReactElement } = {
|
||||
"debug-logs": <DebugLogs />,
|
||||
"sound-picker": <SoundPicker />,
|
||||
licenses: <Licenses />,
|
||||
"trash-interval-selector": <TrashIntervalPicker />,
|
||||
"font-selector": <FontPicker />,
|
||||
"title-format": <TitleFormat />,
|
||||
"date-format-selector": <DateFormatPicker />,
|
||||
"time-format-selector": <TimeFormatPicker />,
|
||||
"day-format-selector": <DayFormatPicker />,
|
||||
"week-format-selector": <WeekFormatPicker />,
|
||||
"image-compression-picker": <ImageCompressionPicker />,
|
||||
"theme-selector": <ThemeSelector />,
|
||||
"applock-timer": <ApplockTimerPicker />,
|
||||
"vault-lock-timer": <VaultLockTimerPicker />,
|
||||
autobackupsattachments: <BackupWithAttachmentsReminderPicker />,
|
||||
backuprestore: <RestoreBackup />,
|
||||
"server-config": <ServersConfiguration />,
|
||||
@@ -89,5 +82,6 @@ export const components: { [name: string]: ReactElement } = {
|
||||
"manage-inbox-keys": <ManageInboxKeys />,
|
||||
"inbox-keys": <InboxKeysList />,
|
||||
"failed-inbox-items": <FailedInboxItems />,
|
||||
"setup-inbox-keys": <SetupInboxKeys />
|
||||
"setup-inbox-keys": <SetupInboxKeys />,
|
||||
"account-card": <AccountCard />
|
||||
};
|
||||
@@ -27,21 +27,22 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import { Notice } from "../../components/ui/notice";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import useTimer from "../../hooks/use-timer";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { hexToRGBA } from "../../utils/colors";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import filesystem from "../../../common/filesystem";
|
||||
import { presentDialog } from "../../../components/dialog/functions";
|
||||
import AppIcon from "../../../components/ui/AppIcon";
|
||||
import { IconButton } from "../../../components/ui/icon-button";
|
||||
import { Notice } from "../../../components/ui/notice";
|
||||
import Heading from "../../../components/ui/typography/heading";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import useTimer from "../../../hooks/use-timer";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { hexToRGBA } from "../../../utils/colors";
|
||||
|
||||
export default function DebugLogs() {
|
||||
const { colors } = useThemeColors();
|
||||
const { seconds, start } = useTimer("debug_logs_timer");
|
||||
const listRef = useRef<FlatList>(null);
|
||||
const currentOffset = useRef(0);
|
||||
const [logs, setLogs] = useState<
|
||||
{
|
||||
key: string;
|
||||
@@ -69,25 +70,27 @@ export default function DebugLogs() {
|
||||
})();
|
||||
}, [currentLog, seconds, start]);
|
||||
|
||||
const currentIndex = currentLog
|
||||
? logs.findIndex((l) => l.key === currentLog.key)
|
||||
: -1;
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({ item }: { item: LogMessage; index: number }) => {
|
||||
const background =
|
||||
item.level === LogLevel.Error || item.level === LogLevel.Fatal
|
||||
? hexToRGBA(colors.error.paragraph, 0.2)
|
||||
: item.level === LogLevel.Warn
|
||||
? hexToRGBA(colors.static.orange, 0.2)
|
||||
: "transparent";
|
||||
const isError =
|
||||
item.level === LogLevel.Error || item.level === LogLevel.Fatal;
|
||||
const isWarn = item.level === LogLevel.Warn;
|
||||
|
||||
const color =
|
||||
item.level === LogLevel.Error || item.level === LogLevel.Fatal
|
||||
? colors.error.paragraph
|
||||
: item.level === LogLevel.Warn
|
||||
const background = "transparent";
|
||||
|
||||
const color = isError
|
||||
? colors.error.paragraph
|
||||
: isWarn
|
||||
? colors.static.black
|
||||
: colors.primary.paragraph;
|
||||
|
||||
return !item ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
activeOpacity={0.6}
|
||||
onLongPress={() => {
|
||||
Clipboard.setString(format(item));
|
||||
ToastManager.show({
|
||||
@@ -97,8 +100,7 @@ export default function DebugLogs() {
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
backgroundColor: background,
|
||||
flexShrink: 1,
|
||||
borderBottomWidth: 1,
|
||||
@@ -123,7 +125,6 @@ export default function DebugLogs() {
|
||||
colors.primary.paragraph,
|
||||
colors.error.paragraph,
|
||||
colors.static.black,
|
||||
colors.static.orange,
|
||||
colors.primary.border
|
||||
]
|
||||
);
|
||||
@@ -160,7 +161,10 @@ export default function DebugLogs() {
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
/**
|
||||
empty */
|
||||
}
|
||||
}, [currentLog?.logs]);
|
||||
|
||||
const copyLogs = React.useCallback(() => {
|
||||
@@ -209,7 +213,8 @@ export default function DebugLogs() {
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
padding: DefaultAppStyles.GAP
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<Notice text={strings.debugNotice()} type="information" />
|
||||
@@ -221,97 +226,143 @@ export default function DebugLogs() {
|
||||
ListHeaderComponent={
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingBottom: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_2,
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between"
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Paragraph>{currentLog.key}</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: Radius.XS,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
name="file-text"
|
||||
iconFamily="notesnook"
|
||||
size={18}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
marginHorizontal: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
const index = logs.findIndex(
|
||||
(l) => l.key === currentLog.key
|
||||
);
|
||||
if (index === 0) return;
|
||||
setCurrentLog(logs[index - 1]);
|
||||
}}
|
||||
size={20}
|
||||
name="chevron-left"
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="SM" lineHeight="100%" numberOfLines={1}>
|
||||
{currentLog.key}
|
||||
</Heading>
|
||||
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
|
||||
{`${currentIndex + 1} / ${logs.length}`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
<View
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
onPress={() => {
|
||||
const index = logs.findIndex(
|
||||
(l) => l.key === currentLog.key
|
||||
);
|
||||
if (index === logs.length - 1) return;
|
||||
setCurrentLog(logs[index + 1]);
|
||||
}}
|
||||
size={20}
|
||||
name="chevron-right"
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
</View>
|
||||
>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
if (currentIndex <= 0) return;
|
||||
setCurrentLog(logs[currentIndex - 1]);
|
||||
}}
|
||||
disabled={currentIndex <= 0}
|
||||
size={16}
|
||||
name="chevron-right"
|
||||
iconFamily="notesnook"
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
transform: [{ rotate: "180deg" }]
|
||||
}}
|
||||
color={
|
||||
currentIndex <= 0
|
||||
? colors.disabled.icon
|
||||
: colors.primary.icon
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
if (currentIndex === logs.length - 1) return;
|
||||
setCurrentLog(logs[currentIndex + 1]);
|
||||
}}
|
||||
disabled={currentIndex === logs.length - 1}
|
||||
size={16}
|
||||
name="chevron-right"
|
||||
iconFamily="notesnook"
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36
|
||||
}}
|
||||
color={
|
||||
currentIndex === logs.length - 1
|
||||
? colors.disabled.icon
|
||||
: colors.primary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={copyLogs}
|
||||
size={20}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
marginRight: 5
|
||||
}}
|
||||
name="content-copy"
|
||||
color={colors.secondary.paragraph}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={downloadLogs}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={20}
|
||||
name="download"
|
||||
color={colors.secondary.paragraph}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={clearLogs}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={20}
|
||||
name="delete"
|
||||
color={colors.secondary.paragraph}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={copyLogs}
|
||||
size={16}
|
||||
name="recovery-key-copy"
|
||||
iconFamily="notesnook"
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: Radius.XS
|
||||
}}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={downloadLogs}
|
||||
size={16}
|
||||
name="download-simple"
|
||||
iconFamily="notesnook"
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36
|
||||
}}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={clearLogs}
|
||||
size={16}
|
||||
name="trash"
|
||||
iconFamily="notesnook"
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36
|
||||
}}
|
||||
color={colors.error.accent}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
@@ -320,6 +371,9 @@ export default function DebugLogs() {
|
||||
width: "100%"
|
||||
}}
|
||||
stickyHeaderIndices={[0]}
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
@@ -24,13 +24,13 @@ import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { DraxProvider, DraxScrollView } from "react-native-drax";
|
||||
import Animated, { FadeInDown, FadeOutDown } from "react-native-reanimated";
|
||||
import PaywallSheet from "../../../components/sheets/paywall";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { Notice } from "../../../components/ui/notice";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import PaywallSheet from "../../../../components/sheets/paywall";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import { Notice } from "../../../../components/ui/notice";
|
||||
import Paragraph from "../../../../components/ui/typography/paragraph";
|
||||
import { ToastManager } from "../../../../services/event-manager";
|
||||
import { AppFontSize } from "../../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../../utils/styles";
|
||||
import { Group } from "./group";
|
||||
import { DragState, useDragState } from "./state";
|
||||
export const ConfigureToolbar = () => {
|
||||
@@ -22,11 +22,11 @@ import * as React from "react";
|
||||
import { View } from "react-native";
|
||||
import { DraxDragWithReceiverEventData, DraxView } from "react-native-drax";
|
||||
import Animated, { Layout } from "react-native-reanimated";
|
||||
import { presentDialog } from "../../../components/dialog/functions";
|
||||
import { IconButton } from "../../../components/ui/icon-button";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { presentDialog } from "../../../../components/dialog/functions";
|
||||
import { IconButton } from "../../../../components/ui/icon-button";
|
||||
import Paragraph from "../../../../components/ui/typography/paragraph";
|
||||
import { getElevationStyle } from "../../../../utils/elevation";
|
||||
import { AppFontSize } from "../../../../utils/size";
|
||||
import { renderTool } from "./common";
|
||||
import { DraggableItem, useDragState } from "./state";
|
||||
import ToolSheet from "./tool-sheet";
|
||||
@@ -35,8 +35,8 @@ import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
|
||||
import type { ToolId } from "@notesnook/editor";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { ToastManager } from "../../../../services/event-manager";
|
||||
import { DefaultAppStyles } from "../../../../utils/styles";
|
||||
|
||||
export const Group = ({
|
||||
item,
|
||||
@@ -21,9 +21,9 @@ import { CURRENT_TOOLBAR_VERSION, migrateToolbar } from "@notesnook/common";
|
||||
import type { ToolbarGroupDefinition } from "@notesnook/editor";
|
||||
import { create } from "zustand";
|
||||
import { StateStorage, persist } from "zustand/middleware";
|
||||
import { db } from "../../../common/database";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { db } from "../../../../common/database";
|
||||
import { MMKV } from "../../../../common/database/mmkv";
|
||||
import { useSettingStore } from "../../../../stores/use-setting-store";
|
||||
import { presets } from "./toolbar-definition";
|
||||
export type ToolDefinition = string | string[];
|
||||
|
||||
@@ -20,12 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import type { ToolId } from "@notesnook/editor";
|
||||
import React, { RefObject } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Pressable } from "../../../components/ui/pressable";
|
||||
import { SvgView } from "../../../components/ui/svg";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { Pressable } from "../../../../components/ui/pressable";
|
||||
import { SvgView } from "../../../../components/ui/svg";
|
||||
import Paragraph from "../../../../components/ui/typography/paragraph";
|
||||
import { presentSheet } from "../../../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../../utils/size";
|
||||
import { DraggableItem, useDragState } from "./state";
|
||||
import {
|
||||
findToolById,
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from "./toolbar-definition";
|
||||
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { DefaultAppStyles } from "../../../../utils/styles";
|
||||
|
||||
export default function ToolSheet({
|
||||
group,
|
||||
@@ -45,7 +45,7 @@ export default function ToolSheet({
|
||||
}) {
|
||||
const { colors } = useThemeColors();
|
||||
const [data] = useDragState((state) => [state.data]);
|
||||
const ungrouped = getUngroupedTools( data) as ToolId[];
|
||||
const ungrouped = getUngroupedTools(data) as ToolId[];
|
||||
|
||||
const renderTool = React.useCallback(
|
||||
(item: ToolId) => {
|
||||
@@ -22,12 +22,12 @@ import * as React from "react";
|
||||
import { View } from "react-native";
|
||||
import { DraxDragWithReceiverEventData, DraxView } from "react-native-drax";
|
||||
import Animated, { Layout } from "react-native-reanimated";
|
||||
import { presentDialog } from "../../../components/dialog/functions";
|
||||
import { IconButton } from "../../../components/ui/icon-button";
|
||||
import { SvgView } from "../../../components/ui/svg";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { presentDialog } from "../../../../components/dialog/functions";
|
||||
import { IconButton } from "../../../../components/ui/icon-button";
|
||||
import { SvgView } from "../../../../components/ui/svg";
|
||||
import Paragraph from "../../../../components/ui/typography/paragraph";
|
||||
import { getElevationStyle } from "../../../../utils/elevation";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../../utils/size";
|
||||
import { renderGroup } from "./common";
|
||||
import { DraggableItem, useDragState } from "./state";
|
||||
import ToolSheet from "./tool-sheet";
|
||||
@@ -37,8 +37,8 @@ import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
|
||||
import type { ToolId } from "@notesnook/editor";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { ToastManager } from "../../../../services/event-manager";
|
||||
import { DefaultAppStyles } from "../../../../utils/styles";
|
||||
|
||||
export const Tool = ({
|
||||
item,
|
||||
@@ -32,7 +32,7 @@ export const presets: { [name: string]: ToolbarGroupDefinition[] } = {
|
||||
custom: []
|
||||
};
|
||||
|
||||
export function findToolById(id: keyof ReturnType<typeof tools> ): {
|
||||
export function findToolById(id: keyof ReturnType<typeof tools>): {
|
||||
title: string;
|
||||
icon: string;
|
||||
} {
|
||||
@@ -51,7 +51,7 @@ export function getToolIcon(id: ToolId, color: string) {
|
||||
|
||||
export function getUngroupedTools(
|
||||
toolDefinition: (string | string[])[][]
|
||||
): string[] {
|
||||
): string[] {
|
||||
const allTools = tools();
|
||||
const keys = Object.keys(allTools);
|
||||
|
||||
@@ -20,11 +20,11 @@ import { LegendList } from "@legendapp/list";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Linking, Platform } from "react-native";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Pressable } from "../../../components/ui/pressable";
|
||||
import Heading from "../../../components/ui/typography/heading";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { LICENSES } from "./license-data";
|
||||
|
||||
type LicenseEntry = {
|
||||
300
apps/mobile/app/screens/settings/components/notesnook-circle.tsx
Normal file
300
apps/mobile/app/screens/settings/components/notesnook-circle.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
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 { CirclePartner, SubscriptionStatus } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import { ActivityIndicator, Image, ScrollView, View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { presentDialog } from "../../../components/dialog/functions";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { Notice } from "../../../components/ui/notice";
|
||||
import Heading from "../../../components/ui/typography/heading";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { eSendEvent, ToastManager } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { eCloseSimpleDialog } from "../../../utils/events";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
let p: any;
|
||||
export const NotesnookCircle = () => {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const isOnTrial =
|
||||
PremiumService.get() &&
|
||||
user?.subscription?.status === SubscriptionStatus.TRIAL;
|
||||
const isFree = !PremiumService.get();
|
||||
const partners = useAsync(db.circle.partners, [], {
|
||||
initialState: () => p
|
||||
});
|
||||
p = partners.result;
|
||||
|
||||
useEffect(() => {
|
||||
if (isFree || isOnTrial) {
|
||||
presentDialog({
|
||||
icon: "warning-circle",
|
||||
centered: true,
|
||||
iconFamily: "notesnook",
|
||||
iconType: "error",
|
||||
title: strings.subscriptionRequired(),
|
||||
paragraph: isFree
|
||||
? strings.freeUserCircleNotice()
|
||||
: strings.trialUserCircleNotice(),
|
||||
positiveText: isFree ? strings.upgradeNow() : strings.close(),
|
||||
positivePress: async () => {
|
||||
if (!isFree) {
|
||||
Navigation.goBack();
|
||||
return;
|
||||
}
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
await sleep(300);
|
||||
Navigation.navigate("PayWall", {
|
||||
context: "logged-in"
|
||||
});
|
||||
return true;
|
||||
},
|
||||
onClose: () => {
|
||||
Navigation.goBack();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
gap: Spacing.LEVEL_3,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="XL">Partner offers</Heading>
|
||||
<Paragraph>
|
||||
Get exclusive discounts from our trusted partners who share our
|
||||
commitment to privacy and user freedom.
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{!isFree && !isOnTrial ? null : (
|
||||
<View>
|
||||
<Paragraph>
|
||||
{isFree
|
||||
? strings.freeUserCircleNotice()
|
||||
: strings.trialUserCircleNotice()}
|
||||
</Paragraph>
|
||||
|
||||
{isOnTrial ? null : (
|
||||
<Button
|
||||
title={strings.upgradePlan()}
|
||||
onPress={() => {
|
||||
Navigation.navigate("PayWall", {
|
||||
canGoBack: true,
|
||||
context: useUserStore.getState().user
|
||||
? "logged-in"
|
||||
: "logged-out"
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{partners.loading ? <ActivityIndicator /> : null}
|
||||
|
||||
{partners.error ? (
|
||||
<Notice type="alert" text={partners.error.message} />
|
||||
) : null}
|
||||
|
||||
{partners.result?.map((item) => (
|
||||
<Partner key={item.id} item={item} available={!isFree && !isOnTrial} />
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const Partner = ({
|
||||
item,
|
||||
available
|
||||
}: {
|
||||
item: CirclePartner;
|
||||
available: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [code, setCode] = useState<string>();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: Radius.MD,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: Spacing.LEVEL_3,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={item.logoBase64}
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24
|
||||
}}
|
||||
/>
|
||||
<Heading>{item.name}</Heading>
|
||||
</View>
|
||||
|
||||
<Paragraph fontSize="XS">{item.longDescription.trim()}</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: Radius.XS,
|
||||
backgroundColor: colors.primary.shade,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8
|
||||
}}
|
||||
>
|
||||
<Paragraph fontSize="XS" color={colors.primary.accent}>
|
||||
{item.offerDescription}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{available ? (
|
||||
<>
|
||||
{!code ? (
|
||||
<Button
|
||||
type="tertiary"
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border
|
||||
}}
|
||||
title={strings.redeemCode()}
|
||||
width="100%"
|
||||
onPress={() => {
|
||||
if (!PremiumService.get()) {
|
||||
Navigation.navigate("PayWall", {
|
||||
canGoBack: true,
|
||||
context: useUserStore.getState().user
|
||||
? "logged-in"
|
||||
: "logged-out"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.circle
|
||||
.redeem(item.id)
|
||||
.then((result) => setCode(result?.code))
|
||||
.catch((e) => ToastManager.error(e));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.tertiary.background,
|
||||
borderRadius: Radius.XS,
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xs}
|
||||
>
|
||||
Discount code
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.primary.heading}
|
||||
fontFamily="SEMI_BOLD"
|
||||
>
|
||||
{code}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title={strings.copy()}
|
||||
type="accent"
|
||||
style={{
|
||||
paddingVertical: 0,
|
||||
height: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
Clipboard.setString(code);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{item.codeRedeemUrl ? (
|
||||
<Paragraph
|
||||
fontSize="XS"
|
||||
color={colors.primary.accent}
|
||||
fontFamily="MEDIUM"
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (item.codeRedeemUrl) {
|
||||
openLinkInBrowser(
|
||||
item.codeRedeemUrl.replace("{{code}}", code)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{strings.claimPromotion.clickHere()}{" "}
|
||||
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
|
||||
{strings.claimPromotion.toClaim()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
217
apps/mobile/app/screens/settings/components/picker/index.tsx
Normal file
217
apps/mobile/app/screens/settings/components/picker/index.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
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, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Menu, MenuItem } from "react-native-material-menu";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { Dialog } from "../../../../components/dialog";
|
||||
import { Pressable } from "../../../../components/ui/pressable";
|
||||
import Paragraph from "../../../../components/ui/typography/paragraph";
|
||||
import { getColorLinearShade } from "../../../../utils/colors";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../../utils/styles";
|
||||
import { verifyUser } from "../../verify-user";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import { Radius, Spacing } from "../../../../common/design/spacing";
|
||||
|
||||
type PickerType = "menu" | "buttons";
|
||||
|
||||
interface PickerOptions<T, B = any> {
|
||||
getValue: () => B;
|
||||
updateValue: (item: T) => Promise<void>;
|
||||
formatValue: (item: T) => any;
|
||||
compareValue: (current: B, item: T) => boolean;
|
||||
getItemKey: (item: T) => string;
|
||||
options: T[];
|
||||
isFeatureAvailable: () => Promise<boolean>;
|
||||
isOptionAvailable: (item: T) => Promise<boolean>;
|
||||
requiresVerification?: () => boolean;
|
||||
onVerify?: () => Promise<boolean>;
|
||||
pickerType?: PickerType;
|
||||
}
|
||||
|
||||
export function SettingsPicker<T>({
|
||||
getValue,
|
||||
updateValue,
|
||||
formatValue,
|
||||
compareValue,
|
||||
options,
|
||||
getItemKey,
|
||||
isFeatureAvailable,
|
||||
isOptionAvailable,
|
||||
requiresVerification = () => false,
|
||||
onVerify,
|
||||
pickerType = "buttons"
|
||||
}: PickerOptions<T>) {
|
||||
const { colors, isDark } = useThemeColors("contextMenu");
|
||||
const menuRef = useRef<any>(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
const [currentValue, setCurrentValue] = useState(getValue());
|
||||
|
||||
const onChange = async (item: T) => {
|
||||
if ((await isFeatureAvailable()) && (await isOptionAvailable(item))) {
|
||||
menuRef.current?.hide();
|
||||
await updateValue(item);
|
||||
setCurrentValue(item);
|
||||
return;
|
||||
}
|
||||
|
||||
menuRef.current?.hide();
|
||||
await updateValue(item);
|
||||
setCurrentValue(item);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={(event) => {
|
||||
setWidth(event.nativeEvent.layout.width);
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{pickerType === "buttons" ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
{options.map((item) => (
|
||||
<Button
|
||||
key={`option-${getItemKey(item)}`}
|
||||
title={formatValue(item)}
|
||||
fontSize={AppFontSize.sm}
|
||||
fontFamily="REGULAR"
|
||||
type={
|
||||
compareValue(currentValue, item)
|
||||
? "shade-plain"
|
||||
: "secondary-outline"
|
||||
}
|
||||
onPress={() => {
|
||||
onChange(item);
|
||||
}}
|
||||
style={{
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
borderRadius: Radius.XS
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{pickerType === "menu" ? (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
animationDuration={200}
|
||||
style={{
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: colors.primary.background,
|
||||
width: width,
|
||||
marginTop: 60,
|
||||
overflow: "hidden",
|
||||
borderWidth: 0.7,
|
||||
borderColor: getColorLinearShade(
|
||||
colors.primary.background,
|
||||
0.07,
|
||||
isDark
|
||||
)
|
||||
}}
|
||||
onRequestClose={() => {
|
||||
menuRef.current?.hide();
|
||||
}}
|
||||
anchor={
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
if (
|
||||
(onVerify && !(await onVerify())) ||
|
||||
!(await isFeatureAvailable())
|
||||
)
|
||||
return;
|
||||
menuRef.current?.show();
|
||||
}}
|
||||
type="secondary"
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Paragraph>{formatValue(currentValue)}</Paragraph>
|
||||
<Icon
|
||||
color={colors.primary.icon}
|
||||
name="menu-down"
|
||||
size={AppFontSize.md}
|
||||
/>
|
||||
</Pressable>
|
||||
}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
|
||||
{options.map((item) => (
|
||||
<MenuItem
|
||||
key={getItemKey(item)}
|
||||
onPress={async () => {
|
||||
if (requiresVerification?.()) {
|
||||
verifyUser("local", () => {
|
||||
onChange(item);
|
||||
});
|
||||
} else {
|
||||
onChange(item);
|
||||
}
|
||||
}}
|
||||
pressColor={colors.primary.hover}
|
||||
style={{
|
||||
backgroundColor: compareValue(currentValue, item)
|
||||
? colors.selected.background
|
||||
: "transparent",
|
||||
width: "100%",
|
||||
maxWidth: width
|
||||
}}
|
||||
textStyle={{
|
||||
fontSize: AppFontSize.sm,
|
||||
fontFamily: "Inter-Regular",
|
||||
color: compareValue(currentValue, item)
|
||||
? colors.primary.accent
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
>
|
||||
{formatValue(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function createSettingsPicker<T, B>(props: PickerOptions<T, B>) {
|
||||
const Selector = () => {
|
||||
return <SettingsPicker {...props} />;
|
||||
};
|
||||
return Selector;
|
||||
}
|
||||
@@ -18,26 +18,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
DATE_FORMATS,
|
||||
DayFormat,
|
||||
TIME_FORMATS,
|
||||
TimeFormat,
|
||||
TrashCleanupInterval,
|
||||
WeekFormat
|
||||
} from "@notesnook/core";
|
||||
import { getFontById, getFonts } from "@notesnook/editor/dist/cjs/utils/font";
|
||||
import dayjs from "dayjs";
|
||||
import { createSettingsPicker } from ".";
|
||||
import { db } from "../../../common/database";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { Settings, useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { MenuItemsList } from "../../../utils/menu-items";
|
||||
import { verifyUserWithApplock } from "../functions";
|
||||
import { db } from "../../../../common/database";
|
||||
import { ToastManager } from "../../../../services/event-manager";
|
||||
import SettingsService from "../../../../services/settings";
|
||||
import {
|
||||
Settings,
|
||||
useSettingStore
|
||||
} from "../../../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../../../stores/use-user-store";
|
||||
import { MenuItemsList } from "../../../../utils/menu-items";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import PaywallSheet from "../../../components/sheets/paywall";
|
||||
import PaywallSheet from "../../../../components/sheets/paywall";
|
||||
|
||||
const DAY_FORMATS = ["short", "long"];
|
||||
const DayFormatFormats = {
|
||||
@@ -128,56 +128,6 @@ export const SidebarTabPicker = createSettingsPicker({
|
||||
isOptionAvailable: async () => true
|
||||
});
|
||||
|
||||
export const TrashIntervalPicker = createSettingsPicker({
|
||||
getValue: () => db.settings.getTrashCleanupInterval(),
|
||||
updateValue: async (item) => {
|
||||
db.settings.setTrashCleanupInterval(item);
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return item === -1
|
||||
? strings.never()
|
||||
: item === 1
|
||||
? strings.reminderRecurringMode.day()
|
||||
: strings.days(item);
|
||||
},
|
||||
getItemKey: (item) => item.toString(),
|
||||
options: [-1, 1, 7, 30, 365] as TrashCleanupInterval[],
|
||||
compareValue: (current, item) => current === item,
|
||||
isFeatureAvailable: async () => true,
|
||||
isOptionAvailable: async () => {
|
||||
const disableTrashFeature = await isFeatureAvailable("disableTrashCleanup");
|
||||
if (!disableTrashFeature.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: disableTrashFeature.error,
|
||||
type: "info",
|
||||
actionText: strings.upgrade(),
|
||||
func: () => {
|
||||
PaywallSheet.present(disableTrashFeature);
|
||||
}
|
||||
});
|
||||
}
|
||||
return disableTrashFeature.isAllowed;
|
||||
}
|
||||
});
|
||||
|
||||
export const DateFormatPicker = createSettingsPicker({
|
||||
getValue: () => db.settings.getDateFormat(),
|
||||
updateValue: async (item) => {
|
||||
db.settings.setDateFormat(item);
|
||||
useSettingStore.setState({
|
||||
dateFormat: item
|
||||
});
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return `${item} (${dayjs().format(item)})`;
|
||||
},
|
||||
getItemKey: (item) => item,
|
||||
options: DATE_FORMATS,
|
||||
compareValue: (current, item) => current === item,
|
||||
isFeatureAvailable: async () => true,
|
||||
isOptionAvailable: async () => true
|
||||
});
|
||||
|
||||
export const DayFormatPicker = createSettingsPicker({
|
||||
getValue: () => db.settings.getDayFormat(),
|
||||
updateValue: async (item) => {
|
||||
@@ -249,7 +199,7 @@ export const BackupReminderPicker = createSettingsPicker<
|
||||
return item === "useroff" ? strings.off() : strings[item]?.();
|
||||
},
|
||||
getItemKey: (item) => item,
|
||||
options: ["useroff", "daily", "weekly", "monthly"],
|
||||
options: ["daily", "weekly", "monthly", "useroff"],
|
||||
compareValue: (current, item) => current === item,
|
||||
requiresVerification: () => {
|
||||
return (
|
||||
@@ -273,7 +223,7 @@ export const BackupWithAttachmentsReminderPicker = createSettingsPicker({
|
||||
: item.slice(0, 1).toUpperCase() + item.slice(1);
|
||||
},
|
||||
getItemKey: (item) => item,
|
||||
options: ["never", "weekly", "monthly"] as Settings["fullBackupReminder"][],
|
||||
options: ["weekly", "monthly", "never"] as Settings["fullBackupReminder"][],
|
||||
compareValue: (current, item) => current === item,
|
||||
requiresVerification: () => {
|
||||
return (
|
||||
@@ -285,61 +235,6 @@ export const BackupWithAttachmentsReminderPicker = createSettingsPicker({
|
||||
isOptionAvailable: async () => true
|
||||
});
|
||||
|
||||
export const ApplockTimerPicker = createSettingsPicker({
|
||||
getValue: () => useSettingStore.getState().settings.appLockTimer,
|
||||
updateValue: async (item) => {
|
||||
SettingsService.set({ appLockTimer: item });
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return item === -1
|
||||
? strings.never()
|
||||
: item === 0 || item === undefined
|
||||
? strings.immediately()
|
||||
: item === 1
|
||||
? strings.minutes(1)
|
||||
: strings.minutes(item);
|
||||
},
|
||||
getItemKey: (item) => item.toString(),
|
||||
options: [-1, 0, 1, 5, 15, 30],
|
||||
compareValue: (current, item) => current === item,
|
||||
onVerify: () => {
|
||||
return verifyUserWithApplock();
|
||||
},
|
||||
isFeatureAvailable: async () => true,
|
||||
isOptionAvailable: async () => true
|
||||
});
|
||||
|
||||
export const VaultLockTimerPicker = createSettingsPicker({
|
||||
getValue: () => useSettingStore.getState().vaultLockAfter,
|
||||
updateValue: async (item) => {
|
||||
await db.settings.setVaultLockAfter(item);
|
||||
useSettingStore.setState({
|
||||
vaultLockAfter: item
|
||||
});
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return item === -1
|
||||
? strings.never()
|
||||
: item < 1000 * 60 * 60
|
||||
? strings.minutes(item / (1000 * 60))
|
||||
: strings.hours(item / (1000 * 60 * 60));
|
||||
},
|
||||
getItemKey: (item) => item.toString(),
|
||||
options: [
|
||||
1000 * 60 * 1,
|
||||
1000 * 60 * 5,
|
||||
1000 * 60 * 10,
|
||||
1000 * 60 * 15,
|
||||
1000 * 60 * 30,
|
||||
1000 * 60 * 45,
|
||||
1000 * 60 * 60,
|
||||
-1
|
||||
],
|
||||
compareValue: (current, item) => current === item,
|
||||
isFeatureAvailable: async () => true,
|
||||
isOptionAvailable: async () => true
|
||||
});
|
||||
|
||||
export const ImageCompressionPicker = createSettingsPicker({
|
||||
getValue: () => useSettingStore.getState().settings.imageCompression,
|
||||
updateValue: async (item) => {
|
||||
@@ -347,9 +242,9 @@ export const ImageCompressionPicker = createSettingsPicker({
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return item === "ask-every-time"
|
||||
? strings.askEveryTime()
|
||||
? strings.alwaysAsk()
|
||||
: item === "enabled"
|
||||
? strings.enableRecommended()
|
||||
? strings.enable()
|
||||
: strings.disable();
|
||||
},
|
||||
getItemKey: (item) => item,
|
||||
247
apps/mobile/app/screens/settings/components/server-config.tsx
Normal file
247
apps/mobile/app/screens/settings/components/server-config.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
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 { isServerCompatible } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { presentDialog } from "../../../components/dialog/functions";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import Input from "../../../components/ui/input";
|
||||
import { Notice } from "../../../components/ui/notice";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { HostId, HostIds } from "../../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { Spacing } from "../../../common/design/spacing";
|
||||
|
||||
export const ServerIds = [
|
||||
"notesnook-sync",
|
||||
"auth",
|
||||
"sse",
|
||||
"monograph"
|
||||
] as const;
|
||||
export type ServerId = (typeof ServerIds)[number];
|
||||
type Server = {
|
||||
id: ServerId;
|
||||
host: HostId;
|
||||
title: string;
|
||||
example: string;
|
||||
description: string;
|
||||
versionEndpoint: string;
|
||||
};
|
||||
type VersionResponse = {
|
||||
version: number;
|
||||
id: string;
|
||||
instance: string;
|
||||
};
|
||||
const SERVERS: Server[] = [
|
||||
{
|
||||
id: "notesnook-sync",
|
||||
host: "API_HOST",
|
||||
title: strings.syncServer(),
|
||||
example: "http://localhost:4326",
|
||||
description: strings.syncServerDesc(),
|
||||
versionEndpoint: "/version"
|
||||
},
|
||||
{
|
||||
id: "auth",
|
||||
host: "AUTH_HOST",
|
||||
title: strings.authServer(),
|
||||
example: "http://localhost:5326",
|
||||
description: strings.authServerDesc(),
|
||||
versionEndpoint: "/version"
|
||||
},
|
||||
{
|
||||
id: "sse",
|
||||
host: "SSE_HOST",
|
||||
title: strings.sseServer(),
|
||||
example: "http://localhost:7326",
|
||||
description: strings.sseServerDesc(),
|
||||
versionEndpoint: "/version"
|
||||
},
|
||||
{
|
||||
id: "monograph",
|
||||
host: "MONOGRAPH_HOST",
|
||||
title: strings.monographServer(),
|
||||
example: "http://localhost:6326",
|
||||
description: strings.monographServerDesc(),
|
||||
versionEndpoint: "/api/version"
|
||||
}
|
||||
];
|
||||
export function ServersConfiguration() {
|
||||
const { colors } = useThemeColors();
|
||||
const [error, setError] = useState<string>();
|
||||
const [success, setSuccess] = useState<boolean>();
|
||||
const [urls, setUrls] = useState<Partial<Record<HostId, string>>>(
|
||||
SettingsService.getProperty("serverUrls") || {}
|
||||
);
|
||||
const isLoggedIn = useUserStore((state) => !!state.user);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{isLoggedIn ? (
|
||||
<Notice text={strings.logoutToChangeServerUrls()} type="information" />
|
||||
) : null}
|
||||
|
||||
{!isLoggedIn ? (
|
||||
<View style={{ flexDirection: "column", gap: Spacing.LEVEL_2 }}>
|
||||
{SERVERS.map((server) => (
|
||||
<Input
|
||||
key={server.id}
|
||||
editable={!isLoggedIn}
|
||||
containerStyle={{
|
||||
borderWidth: 0,
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
placeholder={`${server.id} e.g. ${server.example}`}
|
||||
validationType="url"
|
||||
defaultValue={urls[server.host]}
|
||||
errorMessage={strings.enterValidUrl()}
|
||||
onChangeText={(value) =>
|
||||
setUrls((s) => {
|
||||
s[server.host] = value;
|
||||
return s;
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{error ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
color={colors.error.paragraph}
|
||||
>
|
||||
{error}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{success === true ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
color={colors.success.paragraph}
|
||||
>
|
||||
{strings.connectedToServer()}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: 1,
|
||||
justifyContent: "flex-end",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
disabled={isLoggedIn}
|
||||
type="secondary"
|
||||
width="100%"
|
||||
onPress={async () => {
|
||||
setError(undefined);
|
||||
try {
|
||||
for (const host of HostIds) {
|
||||
const url = urls[host];
|
||||
const server = SERVERS.find((s) => s.host === host)!;
|
||||
if (!server) throw new Error(strings.serverNotFound(host));
|
||||
if (!url) throw new Error(strings.allServerUrlsRequired());
|
||||
const version = await fetch(
|
||||
`${url}${server.versionEndpoint}`
|
||||
)
|
||||
.then((r) => r.json() as Promise<VersionResponse>)
|
||||
.catch(() => undefined);
|
||||
if (!version)
|
||||
throw new Error(
|
||||
`${strings.couldNotConnectTo(server.title)}`
|
||||
);
|
||||
if (version.id !== server.id)
|
||||
throw new Error(
|
||||
`${strings.incorrectServerUrl(url, server.title)}.`
|
||||
);
|
||||
if (!isServerCompatible(version.version)) {
|
||||
throw new Error(
|
||||
strings.serverVersionMismatch(server.title, url)
|
||||
);
|
||||
}
|
||||
}
|
||||
setSuccess(true);
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
}
|
||||
}}
|
||||
title={strings.testConnection()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="accent"
|
||||
disabled={isLoggedIn}
|
||||
width="100%"
|
||||
onPress={async () => {
|
||||
if (!success) {
|
||||
ToastManager.show({
|
||||
heading: strings.testConnectionBeforeSave()
|
||||
});
|
||||
return;
|
||||
}
|
||||
SettingsService.setProperty(
|
||||
"serverUrls",
|
||||
urls as Record<HostId, string>
|
||||
);
|
||||
|
||||
presentDialog({
|
||||
title: strings.serverUrlChanged(),
|
||||
paragraph: strings.restartAppToTakeEffect(),
|
||||
negativeText: strings.done()
|
||||
});
|
||||
}}
|
||||
title={strings.save()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={isLoggedIn}
|
||||
type="error"
|
||||
width="100%"
|
||||
title={strings.resetServerUrls()}
|
||||
onPress={async () => {
|
||||
if (isLoggedIn) return;
|
||||
SettingsService.setProperty("serverUrls", undefined);
|
||||
presentDialog({
|
||||
title: strings.serverUrlsReset(),
|
||||
paragraph: strings.restartAppToTakeEffect(),
|
||||
negativeText: strings.done()
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -24,16 +24,16 @@ import NotificationSounds, {
|
||||
stopSampleSound
|
||||
} from "react-native-notification-sounds";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import Notifications from "../../services/notifications";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { IconButton } from "../../../components/ui/icon-button";
|
||||
import { Pressable } from "../../../components/ui/pressable";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import Notifications from "../../../services/notifications";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
const SoundItem = ({
|
||||
playingSoundId,
|
||||
1124
apps/mobile/app/screens/settings/components/theme-selector.tsx
Normal file
1124
apps/mobile/app/screens/settings/components/theme-selector.tsx
Normal file
File diff suppressed because it is too large
Load Diff
98
apps/mobile/app/screens/settings/components/title-format.tsx
Normal file
98
apps/mobile/app/screens/settings/components/title-format.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
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 { strings } from "@notesnook/intl";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import Input from "../../../components/ui/input";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
const TITLE_FORMAT_TOKENS: { token: string; title: () => string }[] = [
|
||||
{ token: "$date$", title: strings.titleFormatCurrentDate },
|
||||
{ token: "$time$", title: strings.titleFormatCurrentTime },
|
||||
{ token: "$day$", title: strings.titleFormatCurrentDay },
|
||||
{ token: "$timestamp$", title: strings.titleFormatTimestamp },
|
||||
{ token: "$count$", title: strings.titleFormatNoteCount },
|
||||
{ token: "$headline$", title: strings.titleFormatHeadline }
|
||||
];
|
||||
|
||||
export const TitleFormat = () => {
|
||||
const [titleFormat, setTitleFormat] = useState(
|
||||
db.settings.getTitleFormat() || ""
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const updateFormat = (value: string) => {
|
||||
setTitleFormat(value);
|
||||
db.settings.setTitleFormat(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
value={titleFormat}
|
||||
onChangeText={updateFormat}
|
||||
onSubmit={(e) => updateFormat(e.nativeEvent.text)}
|
||||
containerStyle={{
|
||||
borderWidth: 0,
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
inputStyle={{
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{TITLE_FORMAT_TOKENS.map((item) => (
|
||||
<Button
|
||||
key={item.token}
|
||||
title={item.title()}
|
||||
type="secondary"
|
||||
bold={false}
|
||||
fontSize={AppFontSize.xs}
|
||||
onPress={() => updateFormat(`${titleFormat}${item.token}`)}
|
||||
fontFamily="REGULAR"
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_1,
|
||||
borderRadius: Radius.XS,
|
||||
width: undefined,
|
||||
borderWidth: 0
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
316
apps/mobile/app/screens/settings/components/user-section.jsx
Normal file
316
apps/mobile/app/screens/settings/components/user-section.jsx
Normal file
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
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 { formatBytes, getFormattedDate } from "@notesnook/common";
|
||||
import {
|
||||
SubscriptionPlan,
|
||||
SubscriptionProvider,
|
||||
SubscriptionStatus
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import dayjs from "dayjs";
|
||||
import React from "react";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { PlanLimits } from "../../../components/sheets/plan-limits";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import Heading from "../../../components/ui/typography/heading";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { presentSheet, 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 { planToDisplayName } from "../../../utils/constants";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { SectionItem } from "../section-item";
|
||||
|
||||
export const getTimeLeft = (t2) => {
|
||||
let daysRemaining = dayjs(t2).diff(dayjs(), "days");
|
||||
return {
|
||||
time: dayjs(t2).diff(dayjs(), daysRemaining === 0 ? "hours" : "days"),
|
||||
isHour: daysRemaining === 0
|
||||
};
|
||||
};
|
||||
|
||||
const getBillingSubtitle = (user) => {
|
||||
if (!user?.subscription || user.subscription.plan === SubscriptionPlan.FREE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAnnual = user.subscription.productId?.includes("yearly");
|
||||
const billingLabel =
|
||||
user?.subscription?.provider === SubscriptionProvider.STREETWRITERS
|
||||
? "Awarded"
|
||||
: isAnnual
|
||||
? "Billed annually"
|
||||
: user.subscription.productId?.includes("monthly")
|
||||
? "Billed monthly"
|
||||
: null;
|
||||
|
||||
if (!user.subscription.expiry) return billingLabel;
|
||||
|
||||
const expiryLabel = getFormattedDate(user.subscription.expiry, "date");
|
||||
const renewalLabel =
|
||||
user.subscription.status === SubscriptionStatus.CANCELED ||
|
||||
user.subscription.status === SubscriptionStatus.PAUSED ||
|
||||
user.subscription.status === SubscriptionStatus.EXPIRED ||
|
||||
user?.subscription?.provider === SubscriptionProvider.STREETWRITERS
|
||||
? "Ends"
|
||||
: "Renews";
|
||||
|
||||
return [billingLabel, `${renewalLabel} ${expiryLabel}`]
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
};
|
||||
|
||||
const SettingsUserSection = ({ item }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [user] = useUserStore((state) => [state.user]);
|
||||
const used = user?.storageUsed || 0;
|
||||
const total = user?.totalStorage || 0;
|
||||
const storagePercent = total > 0 ? Math.min((used / total) * 100, 100) : 0;
|
||||
const planName = planToDisplayName(user?.subscription?.plan);
|
||||
const billingSubtitle = getBillingSubtitle(user);
|
||||
const showUpgradeButton = !(
|
||||
((user?.subscription?.provider === SubscriptionProvider.PADDLE ||
|
||||
user?.subscription?.provider === SubscriptionProvider.STREETWRITERS ||
|
||||
!(
|
||||
(user?.subscription?.provider === SubscriptionProvider.APPLE &&
|
||||
Platform.OS === "ios") ||
|
||||
(user?.subscription?.provider === SubscriptionProvider.GOOGLE &&
|
||||
Platform.OS === "android")
|
||||
)) &&
|
||||
PremiumService.get()) ||
|
||||
SettingsService.getProperty("serverUrls")
|
||||
);
|
||||
|
||||
const storageText =
|
||||
total === -1
|
||||
? `${formatBytes(used)}/Unlimited ${strings.used()}`
|
||||
: `${formatBytes(used)}/${formatBytes(total)} ${strings.used()}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
{user ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
marginBottom: Spacing.LEVEL_2,
|
||||
marginTop: -Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: Radius.S,
|
||||
padding: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_3,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: Spacing.LEVEL_2,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
fontSize="XL"
|
||||
fontFamily="SEMI_BOLD"
|
||||
lineHeight={null}
|
||||
color={colors.primary.heading}
|
||||
>
|
||||
{planName}
|
||||
</Heading>
|
||||
|
||||
{billingSubtitle ? (
|
||||
<Paragraph fontSize="XS" color={colors.primary.paragraph}>
|
||||
{billingSubtitle}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title={strings.upgrade().toUpperCase()}
|
||||
type="accent"
|
||||
disabled={!showUpgradeButton}
|
||||
fontSize={AppFontSize.sm}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
borderRadius: Radius.XS
|
||||
}}
|
||||
textStyle={{
|
||||
textTransform: "uppercase"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (
|
||||
user?.subscription?.plan === SubscriptionPlan.LEGACY_PRO
|
||||
) {
|
||||
ToastManager.show({
|
||||
message: strings.cannotChangePlan(),
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
user.subscription?.plan !== SubscriptionPlan.FREE &&
|
||||
user.subscription?.productId &&
|
||||
user.subscription?.productId.includes("5year")
|
||||
) {
|
||||
ToastManager.show({
|
||||
message:
|
||||
"You have made a one time purchase. To change your plan please contact support.",
|
||||
type: "info"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.navigate("PayWall", {
|
||||
context: "logged-in",
|
||||
canGoBack: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
gap: Spacing.LEVEL_1,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: Spacing.LEVEL_2,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
fontSize="XS"
|
||||
fontFamily="MEDIUM"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.storage()}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
fontSize="XS"
|
||||
fontFamily="MEDIUM"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{storageText}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.tertiary.background,
|
||||
width: "100%",
|
||||
height: 8,
|
||||
borderRadius: Radius.XXL,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.accent,
|
||||
height: 8,
|
||||
width: `${storagePercent}%`,
|
||||
borderRadius: Radius.XXL
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
onPress={() => {
|
||||
presentSheet({
|
||||
component: <PlanLimits />
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
fontSize="XS"
|
||||
fontFamily="SEMI_BOLD"
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{strings.viewAllLimits()}
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Heading
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
marginBottom: Spacing.LEVEL_2
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.sm}
|
||||
fontFamily="MEDIUM"
|
||||
>
|
||||
{strings.account()}
|
||||
</Heading>
|
||||
|
||||
{item.sections.map((item) => (
|
||||
<SectionItem key={item.name} item={item} />
|
||||
))}
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.separator
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsUserSection;
|
||||
@@ -23,15 +23,17 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { FontSizes } from "../../common/design/font";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import { db } from "../../common/database";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import { Header } from "../../components/header";
|
||||
import AppIcon from "../../components/ui/AppIcon";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Header } from "../../components/header";
|
||||
|
||||
type FailedInboxItem = {
|
||||
id: string;
|
||||
@@ -51,50 +53,7 @@ function parseErrorContext(
|
||||
}
|
||||
}
|
||||
|
||||
function ErrorBadge({
|
||||
message
|
||||
}: {
|
||||
message: InboxItemsHistoryErrorContext["message"] | undefined;
|
||||
}) {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
if (!message) {
|
||||
return (
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
N/A
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
const palette =
|
||||
message === "Invalid JSON"
|
||||
? { background: "rgba(255, 152, 0, 0.15)", paragraph: "#e65100" }
|
||||
: message === "Validation failed"
|
||||
? { background: "rgba(255, 193, 7, 0.15)", paragraph: "#8a6000" }
|
||||
: colors.error;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
backgroundColor: palette.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
color={palette.paragraph}
|
||||
style={{ fontWeight: "700" }}
|
||||
>
|
||||
{message}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailsBlock({ value }: { value: string }) {
|
||||
function PayloadDataBlock({ value }: { value: string }) {
|
||||
const { colors } = useThemeColors();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -107,25 +66,27 @@ function DetailsBlock({ value }: { value: string }) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.border,
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: colors.secondary.background,
|
||||
overflow: "hidden"
|
||||
borderRadius: Radius.S,
|
||||
padding: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_2,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.secondary.border
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name={copied ? "check" : "content-copy"}
|
||||
color={colors.primary.icon}
|
||||
size={AppFontSize.lg}
|
||||
<Heading fontSize="MD" lineHeight="100%">
|
||||
{strings.payloadData()}
|
||||
</Heading>
|
||||
|
||||
<Pressable
|
||||
type="transparent"
|
||||
onPress={() => {
|
||||
try {
|
||||
Clipboard.setString(value);
|
||||
@@ -141,24 +102,56 @@ function DetailsBlock({ value }: { value: string }) {
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={{ maxHeight: 120 }}
|
||||
contentContainerStyle={{
|
||||
padding: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
style={{
|
||||
fontFamily: "monospace"
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: Spacing.LEVEL_1,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
|
||||
flexShrink: 1,
|
||||
width: undefined
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Paragraph>
|
||||
</ScrollView>
|
||||
<AppIcon
|
||||
name={copied ? "check" : "copy"}
|
||||
iconFamily={"notesnook"}
|
||||
size={16}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<Paragraph
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="SM"
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{strings.copy()}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={{ height: 1, backgroundColor: colors.primary.border }} />
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.tertiary.background,
|
||||
borderRadius: Radius.S,
|
||||
padding: Spacing.LEVEL_2,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
style={{ maxHeight: 160, width: "100%" }}
|
||||
nestedScrollEnabled
|
||||
>
|
||||
<Paragraph
|
||||
fontSize="SM"
|
||||
color={colors.primary.paragraph}
|
||||
style={{ fontFamily: "monospace" }}
|
||||
>
|
||||
{value}
|
||||
</Paragraph>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -197,7 +190,9 @@ export const FailedInboxItems = () => {
|
||||
rightButton={
|
||||
items?.length > 0
|
||||
? {
|
||||
name: "delete",
|
||||
name: "trash",
|
||||
iconFamily: "notesnook",
|
||||
size: 20,
|
||||
color: colors.primary.icon,
|
||||
onPress: async () => {
|
||||
presentDialog({
|
||||
@@ -226,7 +221,7 @@ export const FailedInboxItems = () => {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size="small" color={colors.primary.accent} />
|
||||
@@ -242,8 +237,8 @@ export const FailedInboxItems = () => {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.error.paragraph}>
|
||||
@@ -260,32 +255,25 @@ export const FailedInboxItems = () => {
|
||||
{items.length === 0 ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_2,
|
||||
flex: 1,
|
||||
justifyContent: "center"
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{strings.noFailedInboxItems()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{strings.noFailedInboxItems()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{result.status === "fulfilled" && items.length > 0 ? (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
gap: Spacing.LEVEL_4,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: Spacing.LEVEL_4,
|
||||
paddingBottom: 50
|
||||
}}
|
||||
>
|
||||
@@ -304,96 +292,78 @@ export const FailedInboxItems = () => {
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.border,
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: colors.primary.background,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
gap: Spacing.LEVEL_3,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{getFormattedDate(item.dateSynced, "date-time")}
|
||||
</Paragraph>
|
||||
<View style={{ gap: Spacing.LEVEL_2, width: "100%" }}>
|
||||
<View style={{ gap: Spacing.LEVEL_1, width: "100%" }}>
|
||||
<Heading fontSize="LG" lineHeight="100%">
|
||||
{(message as string) || strings.failed()}
|
||||
</Heading>
|
||||
{description ? (
|
||||
<Paragraph fontSize="SM" color={colors.primary.paragraph}>
|
||||
{description as string}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<ErrorBadge message={message} />
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.error.background,
|
||||
padding: 3,
|
||||
paddingHorizontal: 6,
|
||||
borderRadius: 4,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap"
|
||||
gap: Spacing.LEVEL_0 + 2
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.error.paragraph}>Error</Paragraph>
|
||||
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
|
||||
{getFormattedDate(item.dateSynced, "date")}
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: Radius.MD,
|
||||
backgroundColor: colors.secondary.paragraph
|
||||
}}
|
||||
/>
|
||||
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
|
||||
{getFormattedDate(item.dateSynced, "time")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{(description as string) || "N/A"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{details ? (
|
||||
<DetailsBlock value={details} />
|
||||
) : (
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
N/A
|
||||
</Paragraph>
|
||||
)}
|
||||
{details ? <PayloadDataBlock value={details} /> : null}
|
||||
|
||||
<View style={{ alignItems: "flex-end" }}>
|
||||
<Button
|
||||
title={strings.delete()}
|
||||
type="error"
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
title: strings.delete(),
|
||||
paragraph: strings.areYouSure(),
|
||||
positiveText: strings.delete(),
|
||||
positiveType: "error",
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
try {
|
||||
await deleteItem(item.id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ToastManager.error(error as Error);
|
||||
return false;
|
||||
}
|
||||
<Button
|
||||
title={strings.delete()}
|
||||
type="error"
|
||||
fontSize={FontSizes.MD}
|
||||
style={{
|
||||
width: "100%",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.error.border,
|
||||
borderRadius: Radius.XS,
|
||||
paddingVertical: Spacing.LEVEL_3
|
||||
}}
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
title: strings.delete(),
|
||||
paragraph: strings.areYouSure(),
|
||||
positiveText: strings.delete(),
|
||||
positiveType: "error",
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
try {
|
||||
await deleteItem(item.id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ToastManager.error(error as Error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,159 +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 { strings } from "@notesnook/intl";
|
||||
import { db } from "../../common/database";
|
||||
import { validateAppLockPassword } from "../../common/database/encryption";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { sleep } from "../../utils/time";
|
||||
|
||||
export async function verifyUser(
|
||||
context,
|
||||
onsuccess,
|
||||
disableBackdropClosing,
|
||||
onclose,
|
||||
closeText
|
||||
) {
|
||||
presentDialog({
|
||||
context: context,
|
||||
title: strings.verifyItsYou(),
|
||||
input: true,
|
||||
inputPlaceholder: strings.enterPassword(),
|
||||
paragraph: strings.enterPasswordDesc(),
|
||||
positiveText: strings.verify(),
|
||||
secureTextEntry: true,
|
||||
disableBackdropClosing: disableBackdropClosing,
|
||||
onClose: onclose,
|
||||
negativeText: closeText || strings.cancel(),
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
if (!value || !value.trim()) {
|
||||
ToastManager.error(
|
||||
new Error(strings.passwordNotEntered()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const user = await db.user.getUser();
|
||||
let verified = !user ? true : await db.user.verifyPassword(value);
|
||||
if (verified) {
|
||||
sleep(300).then(async () => {
|
||||
await onsuccess();
|
||||
});
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
heading: strings.verifyFailed(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyUserWithApplock() {
|
||||
const keyboardType = SettingsService.getProperty("applockKeyboardType");
|
||||
return new Promise((resolve) => {
|
||||
if (SettingsService.getProperty("appLockHasPasswordSecurity")) {
|
||||
presentDialog({
|
||||
title: strings.verifyItsYou(),
|
||||
input: true,
|
||||
inputPlaceholder:
|
||||
keyboardType == "numeric"
|
||||
? strings.enterApplockPin()
|
||||
: strings.enterApplockPassword(),
|
||||
paragraph:
|
||||
keyboardType == "numeric"
|
||||
? strings.enterApplockPinDesc()
|
||||
: strings.enterApplockPasswordDesc(),
|
||||
positiveText: strings.verify(),
|
||||
secureTextEntry: true,
|
||||
negativeText: strings.cancel(),
|
||||
keyboardType: keyboardType,
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
if (!value || !value.trim()) {
|
||||
ToastManager.error(
|
||||
new Error(strings.passwordNotEntered()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const verified = await validateAppLockPassword(value);
|
||||
if (!verified) {
|
||||
ToastManager.show({
|
||||
heading: strings.invalid(
|
||||
keyboardType === "numeric" ? "pin" : "password"
|
||||
),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
resolve(verified);
|
||||
} catch (e) {
|
||||
resolve(false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
BiometricService.isBiometryAvailable().then((available) => {
|
||||
if (available) {
|
||||
BiometricService.validateUser(strings.verifyItsYou()).then(
|
||||
(verified) => {
|
||||
resolve(verified);
|
||||
}
|
||||
);
|
||||
} else if (useUserStore.getState().user) {
|
||||
let verified = false;
|
||||
verifyUser(
|
||||
null,
|
||||
() => {
|
||||
resolve(true);
|
||||
},
|
||||
false,
|
||||
() => {
|
||||
resolve(verified);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -26,10 +26,12 @@ import DelayLayout from "../../components/delay-layout";
|
||||
import { Header } from "../../components/header";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { components } from "./components";
|
||||
import { components } from "./components/components";
|
||||
import { SectionItem } from "./section-item";
|
||||
import { RouteParams, SettingSection } from "./types";
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SectionGroup } from "./section-group";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
const keyExtractor = (item: SettingSection) => item.id;
|
||||
const AnimatedKeyboardAvoidingFlatList = Animated.createAnimatedComponent(
|
||||
KeyboardAwareFlatList
|
||||
@@ -39,15 +41,25 @@ const Group = ({
|
||||
navigation,
|
||||
route
|
||||
}: NativeStackScreenProps<RouteParams, "SettingsGroup">) => {
|
||||
const { colors } = useThemeColors();
|
||||
useNavigationFocus(navigation, {
|
||||
onFocus: () => {
|
||||
useNavigationStore.getState().setFocusedRouteId("Settings");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const renderItem = ({ item }: { item: SettingSection; index: number }) => (
|
||||
<SectionItem item={item} />
|
||||
);
|
||||
const renderItem = ({
|
||||
item,
|
||||
index
|
||||
}: {
|
||||
item: SettingSection;
|
||||
index: number;
|
||||
}) =>
|
||||
item.type === "group" ? (
|
||||
<SectionGroup item={item} isLast={!route.params.sections?.[index + 1]} />
|
||||
) : (
|
||||
<SectionItem item={item} />
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -56,6 +68,12 @@ const Group = ({
|
||||
renderedInRoute="Settings"
|
||||
title={route.params.name as string}
|
||||
canGoBack={true}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border
|
||||
}}
|
||||
id="Settings"
|
||||
/>
|
||||
)}
|
||||
@@ -65,10 +83,21 @@ const Group = ({
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{route.params.component ? components[route.params.component] : null}
|
||||
{!route.params.sections && route.params.component
|
||||
? components[route.params.component]
|
||||
: null}
|
||||
|
||||
{route.params.sections ? (
|
||||
<AnimatedKeyboardAvoidingFlatList
|
||||
data={route.params.sections}
|
||||
ListHeaderComponent={
|
||||
route.params.component
|
||||
? components[route.params.component]
|
||||
: null
|
||||
}
|
||||
contentContainerStyle={{
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
enableOnAndroid
|
||||
|
||||
88
apps/mobile/app/screens/settings/groups/about.tsx
Normal file
88
apps/mobile/app/screens/settings/groups/about.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
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 React from "react";
|
||||
import { Linking } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import { Update } from "../../../components/sheets/update";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { SettingSection } from "../types";
|
||||
|
||||
export const aboutGroup: SettingSection = {
|
||||
id: "about",
|
||||
name: strings.about(),
|
||||
sections: [
|
||||
{
|
||||
id: "download",
|
||||
name: strings.downloadOnDesktop(),
|
||||
icon: "download-simple",
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
try {
|
||||
await Linking.openURL("https://notesnook.com/downloads");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
description: strings.downloadOnDesktopDesc()
|
||||
},
|
||||
{
|
||||
id: "roadmap",
|
||||
name: strings.roadmap(),
|
||||
icon: "chart-line-up",
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
try {
|
||||
await Linking.openURL("https://notesnook.com/roadmap/");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
description: strings.roadmapDesc()
|
||||
},
|
||||
{
|
||||
id: "check-for-updates",
|
||||
name: strings.checkForUpdates(),
|
||||
icon: "device-mobile-camera",
|
||||
iconFamily: "notesnook",
|
||||
description: strings.checkForUpdatesDesc(),
|
||||
modifer: async () => {
|
||||
presentSheet({
|
||||
//@ts-ignore // Migrate to ts
|
||||
component: (ref) => <Update fwdRef={ref} />
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "app-version",
|
||||
name: strings.appVersion(),
|
||||
icon: "github-logo",
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
try {
|
||||
await Linking.openURL("https://notesnook.com");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
description: getVersion()
|
||||
}
|
||||
]
|
||||
};
|
||||
74
apps/mobile/app/screens/settings/groups/account-local.ts
Normal file
74
apps/mobile/app/screens/settings/groups/account-local.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
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 { db } from "../../../common/database";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { presentDialog } from "../../../components/dialog/functions";
|
||||
import BiometricService from "../../../services/biometrics";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { clearAllStores } from "../../../stores";
|
||||
import { refreshAllStores } from "../../../stores/create-db-collection-store";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { resetTabStore } from "../../editor/tiptap/use-tab-store";
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import { eAfterSync } from "../../../utils/events";
|
||||
import { SettingSection } from "../types";
|
||||
|
||||
export const accountLocalGroup: SettingSection = {
|
||||
id: "account-local",
|
||||
name: strings.account(),
|
||||
useHook: () => useUserStore((state) => state.user),
|
||||
hidden: (current) => !!current,
|
||||
sections: [
|
||||
{
|
||||
id: "delete-data",
|
||||
name: strings.deleteData(),
|
||||
icon: "trash",
|
||||
iconFamily: "notesnook",
|
||||
description: strings.deleteAccountDesc(),
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: strings.deleteData(),
|
||||
paragraph: strings.irreverisibleAction(),
|
||||
positiveType: "errorShade",
|
||||
positiveText: "Delete data",
|
||||
positivePress: async () => {
|
||||
await PremiumService.setPremiumStatus();
|
||||
await BiometricService.resetCredentials();
|
||||
MMKV.clearStore();
|
||||
resetTabStore();
|
||||
clearAllStores();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
SettingsService.resetSettings();
|
||||
db.reset();
|
||||
|
||||
setImmediate(() => {
|
||||
refreshAllStores();
|
||||
eSendEvent(eAfterSync);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
842
apps/mobile/app/screens/settings/groups/account.ts
Normal file
842
apps/mobile/app/screens/settings/groups/account.ts
Normal file
@@ -0,0 +1,842 @@
|
||||
/*
|
||||
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 { formatBytes } from "@notesnook/common";
|
||||
import { SubscriptionPlan, User } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { TextInput } from "react-native-gesture-handler";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { DatabaseLogger, db } from "../../../common/database";
|
||||
import filesystem from "../../../common/filesystem";
|
||||
import { presentDialog } from "../../../components/dialog/functions";
|
||||
import {
|
||||
endProgress,
|
||||
startProgress
|
||||
} from "../../../components/dialogs/progress";
|
||||
import ForceSyncSheet from "../../../components/sheets/force-sync";
|
||||
import { BackgroundSync } from "../../../services/background-sync";
|
||||
import BiometricService from "../../../services/biometrics";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../../components/ui/input/form-input";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { MFARecoveryCodes, MFASheet } from "../components/2fa";
|
||||
import { verifyUser } from "../verify-user";
|
||||
import { logoutUser } from "../logout";
|
||||
import { SettingSection } from "../types";
|
||||
import RecoveryKeySheet from "../../../components/sheets/recovery-key";
|
||||
|
||||
export const accountGroup: SettingSection = {
|
||||
id: "account",
|
||||
name: strings.account(),
|
||||
useHook: () => useUserStore((state) => state.user),
|
||||
hidden: (current) => !current,
|
||||
sections: [
|
||||
// {
|
||||
// id: "subscription-status",
|
||||
// useHook: () => useUserStore((state) => state.user),
|
||||
// hidden: (current) => {
|
||||
// const user = current as User;
|
||||
// return (
|
||||
// !user ||
|
||||
// !user.subscription ||
|
||||
// user.subscription.provider === undefined ||
|
||||
// !strings.subscriptionProviderInfo[user?.subscription?.provider] ||
|
||||
// user.subscription?.plan === SubscriptionPlan.FREE
|
||||
// );
|
||||
// },
|
||||
// name: (current) => {
|
||||
// const user = (current as User) || useUserStore.getState().user;
|
||||
// return (
|
||||
// strings.subscriptionProviderInfo[
|
||||
// user?.subscription?.provider
|
||||
// ]?.title() || `Unknown provider id: ${user?.subscription?.provider}`
|
||||
// );
|
||||
// },
|
||||
// icon: "credit-card",
|
||||
// modifer: () => {
|
||||
// const user = useUserStore.getState().user;
|
||||
// if (!user) return;
|
||||
// const subscriptionProviderInfo =
|
||||
// strings.subscriptionProviderInfo[user?.subscription?.provider];
|
||||
|
||||
// if (!subscriptionProviderInfo) return;
|
||||
|
||||
// const isCurrentPlatform =
|
||||
// (user.subscription?.provider === SubscriptionProvider.APPLE &&
|
||||
// Platform.OS === "ios") ||
|
||||
// (user.subscription?.provider === SubscriptionProvider.GOOGLE &&
|
||||
// Platform.OS === "android");
|
||||
|
||||
// if (
|
||||
// (user.subscription?.provider === SubscriptionProvider.GOOGLE ||
|
||||
// user.subscription?.provider === SubscriptionProvider.APPLE) &&
|
||||
// isCurrentPlatform &&
|
||||
// user?.subscription?.productId
|
||||
// ) {
|
||||
// RNIap.deepLinkToSubscriptions({
|
||||
// sku: user?.subscription.productId
|
||||
// });
|
||||
// } else {
|
||||
// presentSheet({
|
||||
// title: subscriptionProviderInfo.title(),
|
||||
// paragraph: subscriptionProviderInfo.desc()
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// description: (current) => {
|
||||
// const user = current as User;
|
||||
// if (!user) return strings.neverHesitate();
|
||||
// const subscriptionDaysLeft =
|
||||
// user && getTimeLeft(user.subscription?.expiry);
|
||||
// const expiryDate = dayjs(user?.subscription?.expiry).format(
|
||||
// "dddd, MMMM D, YYYY h:mm A"
|
||||
// );
|
||||
// const startDate = dayjs(user?.subscription?.start).format(
|
||||
// "dddd, MMMM D, YYYY h:mm A"
|
||||
// );
|
||||
|
||||
// const trialEndDate = dayjs(user?.subscription?.start)
|
||||
// .add(
|
||||
// user?.subscription?.productId?.includes("monthly") ? 7 : 14,
|
||||
// "day"
|
||||
// )
|
||||
// .format("dddd, MMMM D, YYYY h:mm A");
|
||||
|
||||
// if (
|
||||
// user.subscription?.plan !== SubscriptionPlan.FREE &&
|
||||
// user.subscription?.productId
|
||||
// ) {
|
||||
// const status = user.subscription?.status;
|
||||
// return status === SubscriptionStatus.TRIAL
|
||||
// ? strings.trialOnGoing(trialEndDate)
|
||||
// : status === SubscriptionStatus.ACTIVE
|
||||
// ? strings.subRenewOn(expiryDate)
|
||||
// : status === SubscriptionStatus.CANCELED ||
|
||||
// status === SubscriptionStatus.PAUSED
|
||||
// ? strings.subEndsOn(expiryDate)
|
||||
// : status === SubscriptionStatus.EXPIRED
|
||||
// ? subscriptionDaysLeft.time < -3
|
||||
// ? strings.subEnded()
|
||||
// : strings.accountDowngradedIn(3)
|
||||
// : strings.neverHesitate();
|
||||
// }
|
||||
|
||||
// return strings.neverHesitate();
|
||||
// }
|
||||
// },
|
||||
{
|
||||
id: "redeem-gift-code",
|
||||
name: strings.redeemGiftCode(),
|
||||
description: strings.redeemGiftCodeDesc(),
|
||||
hidden: (current) => {
|
||||
return !current as boolean;
|
||||
},
|
||||
useHook: () =>
|
||||
useUserStore(
|
||||
(state) => state.user?.subscription?.plan === SubscriptionPlan.FREE
|
||||
),
|
||||
icon: "gift",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: strings.redeemGiftCode(),
|
||||
paragraph: strings.redeemGiftCodeDesc(),
|
||||
form: {
|
||||
formRef: createFormRef({
|
||||
code: ""
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
name: "code",
|
||||
placeholder: strings.code(),
|
||||
ref: React.createRef<TextInput | null>(),
|
||||
validators: [validators.required(strings.giftCodeRequired())]
|
||||
}
|
||||
],
|
||||
onFormSubmit: async (form) => {
|
||||
try {
|
||||
await db.subscriptions.redeemCode(form.getValue("code"));
|
||||
return true;
|
||||
} catch (e) {
|
||||
form.setError("code", (e as Error).message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
positiveText: strings.redeem()
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "account-settings",
|
||||
type: "screen",
|
||||
name: strings.manageAccount(),
|
||||
component: "account-card",
|
||||
icon: "user",
|
||||
iconFamily: "notesnook",
|
||||
description: strings.manageAccountDesc(),
|
||||
sections: [
|
||||
{
|
||||
id: "account-sub-section",
|
||||
type: "group",
|
||||
name: strings.account(),
|
||||
icon: "user",
|
||||
iconFamily: "notesnook",
|
||||
sections: [
|
||||
{
|
||||
id: "edit-profile",
|
||||
type: "screen",
|
||||
name: strings.editProfile(),
|
||||
description: strings.editProfileDesc(),
|
||||
icon: "user",
|
||||
iconFamily: "notesnook",
|
||||
sections: [
|
||||
{
|
||||
id: "remove-profile-picture",
|
||||
name: strings.removeProfilePicture(),
|
||||
description: strings.removeProfilePictureDesc(),
|
||||
useHook: () =>
|
||||
useUserStore((state) => state.profile?.profilePicture),
|
||||
hidden: () =>
|
||||
!useUserStore.getState().profile?.profilePicture,
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: strings.removeProfilePicture(),
|
||||
paragraph: strings.removeProfilePictureConfirmation(),
|
||||
positiveText: strings.remove(),
|
||||
positivePress: async () => {
|
||||
db.settings
|
||||
.setProfile({
|
||||
profilePicture: undefined
|
||||
})
|
||||
.then(async () => {
|
||||
useUserStore.setState({
|
||||
profile: db.settings.getProfile()
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "remove-name",
|
||||
name: strings.removeFullName(),
|
||||
description: strings.removeFullNameDesc(),
|
||||
useHook: () =>
|
||||
useUserStore((state) => state.profile?.fullName),
|
||||
hidden: () => !useUserStore.getState().profile?.fullName,
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: strings.removeFullName(),
|
||||
paragraph: strings.removeFullNameConfirmation(),
|
||||
positiveText: strings.remove(),
|
||||
positivePress: async () => {
|
||||
db.settings
|
||||
.setProfile({
|
||||
fullName: undefined
|
||||
})
|
||||
.then(async () => {
|
||||
useUserStore.setState({
|
||||
profile: db.settings.getProfile()
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "change-email",
|
||||
name: strings.changeEmail(),
|
||||
type: "screen",
|
||||
component: "change-email",
|
||||
description: strings.changeEmailDesc(),
|
||||
icon: "at"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "subscription-not-active",
|
||||
name: strings.subscriptionNotActivated(),
|
||||
icon: "warning-circle",
|
||||
iconFamily: "notesnook",
|
||||
hidden: () => Platform.OS !== "ios",
|
||||
modifer: async () => {
|
||||
if (Platform.OS === "android") return;
|
||||
presentSheet({
|
||||
title: strings.loadingSubscription(),
|
||||
paragraph: strings.loadingSubscriptionDesc()
|
||||
});
|
||||
const subscriptions = await RNIap.getPurchaseHistory();
|
||||
subscriptions.sort(
|
||||
(a, b) => b.transactionDate - a.transactionDate
|
||||
);
|
||||
const currentSubscription = subscriptions[0];
|
||||
presentSheet({
|
||||
title: strings.notesnookPro(),
|
||||
paragraph: strings.subscribedOnVerify(
|
||||
new Date(
|
||||
currentSubscription.transactionDate
|
||||
).toLocaleString()
|
||||
),
|
||||
action: async () => {
|
||||
presentSheet({
|
||||
title: strings.verifySubscription(),
|
||||
paragraph: strings.subscriptionVerifyWait()
|
||||
});
|
||||
await PremiumService.subscriptions.verify(
|
||||
currentSubscription
|
||||
);
|
||||
eSendEvent(eCloseSheet);
|
||||
},
|
||||
icon: "information-outline",
|
||||
actionText: strings.verify()
|
||||
});
|
||||
},
|
||||
description: strings.verifySubDesc()
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "security-sub-section",
|
||||
type: "group",
|
||||
name: strings.privacyAndSecurity(),
|
||||
icon: "two-factor-authentication",
|
||||
sections: [
|
||||
{
|
||||
id: "change-password",
|
||||
name: strings.changePassword(),
|
||||
type: "screen",
|
||||
description: strings.changePasswordDesc(),
|
||||
component: "change-password",
|
||||
icon: "pencil-simple-line",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "2fa-settings",
|
||||
type: "screen",
|
||||
name: strings.twoFactorAuth(),
|
||||
description: strings.twoFactorAuthDesc(),
|
||||
icon: "shield-check",
|
||||
iconFamily: "notesnook",
|
||||
sections: [
|
||||
{
|
||||
id: "2fa-settings-group",
|
||||
name: strings.twoFactorAuth(),
|
||||
type: "group",
|
||||
sections: [
|
||||
{
|
||||
id: "enable-2fa",
|
||||
name: strings.change2faMethod(),
|
||||
icon: "shield-check",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
verifyUser("global", async () => {
|
||||
MFASheet.present();
|
||||
});
|
||||
},
|
||||
useHook: () => useUserStore((state) => state.user),
|
||||
description: strings.change2faMethodDesc()
|
||||
},
|
||||
{
|
||||
id: "2fa-fallback",
|
||||
name: strings.addFallback2faMethod(),
|
||||
icon: "shield-plus",
|
||||
iconFamily: "notesnook",
|
||||
useHook: () => useUserStore((state) => state.user),
|
||||
hidden: (user) => {
|
||||
return (
|
||||
!!(user as User)?.mfa?.secondaryMethod ||
|
||||
!(user as User)?.mfa?.isEnabled
|
||||
);
|
||||
},
|
||||
modifer: () => {
|
||||
verifyUser("global", async () => {
|
||||
MFASheet.present(true);
|
||||
});
|
||||
},
|
||||
description: strings.addFallback2faMethodDesc()
|
||||
},
|
||||
{
|
||||
id: "change-2fa-method",
|
||||
name: strings.change2faFallbackMethod(),
|
||||
icon: "shield-plus",
|
||||
useHook: () => useUserStore((state) => state.user),
|
||||
hidden: (user) => {
|
||||
return (
|
||||
!(user as User)?.mfa?.secondaryMethod ||
|
||||
!(user as User)?.mfa?.isEnabled
|
||||
);
|
||||
},
|
||||
modifer: () => {
|
||||
verifyUser("global", async () => {
|
||||
MFASheet.present(true);
|
||||
});
|
||||
},
|
||||
description: strings.change2faFallbackMethod()
|
||||
},
|
||||
{
|
||||
id: "view-2fa-codes",
|
||||
name: strings.viewRecoveryCodes(),
|
||||
modifer: () => {
|
||||
verifyUser("global", async () => {
|
||||
MFARecoveryCodes.present("sms");
|
||||
});
|
||||
},
|
||||
icon: "numpad",
|
||||
iconFamily: "notesnook",
|
||||
useHook: () => useUserStore((state) => state.user),
|
||||
hidden: (user) => {
|
||||
return !(user as User)?.mfa?.isEnabled;
|
||||
},
|
||||
description: strings.viewRecoveryCodesDesc()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "recovery-key",
|
||||
name: strings.saveDataRecoveryKey(),
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
// if (await verifyUser()) {
|
||||
|
||||
// }
|
||||
RecoveryKeySheet.present();
|
||||
},
|
||||
description: strings.saveDataRecoveryKeyDesc(),
|
||||
icon: "key"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "data-storage-sub-section",
|
||||
type: "group",
|
||||
name: strings.dataAndStorage(),
|
||||
icon: "paperclip",
|
||||
iconFamily: "notesnook",
|
||||
sections: [
|
||||
{
|
||||
id: "manage-attachments",
|
||||
name: strings.manageAttachments(),
|
||||
icon: "paperclip",
|
||||
iconFamily: "notesnook",
|
||||
type: "screen",
|
||||
component: "attachments-manager",
|
||||
description: strings.manageAttachmentsDesc(),
|
||||
hideHeader: true
|
||||
},
|
||||
{
|
||||
id: "clear-cache",
|
||||
name: strings.clearCache(),
|
||||
icon: "trash",
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
presentDialog({
|
||||
title: strings.clearCacheConfirm(),
|
||||
paragraph: strings.clearCacheConfirmDesc(),
|
||||
positiveText: strings.clear(),
|
||||
positivePress: async () => {
|
||||
filesystem.clearCache();
|
||||
ToastManager.show({
|
||||
heading: strings.cacheCleared(),
|
||||
message: strings.cacheClearedDesc(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
description(current) {
|
||||
return strings.clearCacheDesc(current as number);
|
||||
},
|
||||
useHook: () => {
|
||||
const [cacheSize, setCacheSize] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
filesystem
|
||||
.getCacheSize()
|
||||
.then(setCacheSize)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
const sub = eSubscribeEvent("cache-cleared", () => {
|
||||
setCacheSize(0);
|
||||
});
|
||||
return () => {
|
||||
sub?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
return formatBytes(cacheSize);
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "account-actions-sub-section",
|
||||
type: "group",
|
||||
name: strings.accountActions(),
|
||||
icon: "user-sheet-logout",
|
||||
iconFamily: "notesnook",
|
||||
sections: [
|
||||
{
|
||||
id: "logout",
|
||||
type: "danger",
|
||||
name: strings.logout(),
|
||||
description: strings.logoutWarnin(),
|
||||
icon: "user-sheet-logout",
|
||||
iconFamily: "notesnook",
|
||||
modifer: logoutUser
|
||||
},
|
||||
{
|
||||
id: "delete-account",
|
||||
type: "danger",
|
||||
name: strings.deleteAccount(),
|
||||
icon: "user-circle-minus",
|
||||
iconFamily: "notesnook",
|
||||
description: strings.deleteAccountDesc(),
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: strings.deleteAccount(),
|
||||
paragraphColor: "red",
|
||||
paragraph: strings.deleteAccountDesc(),
|
||||
positiveType: "errorShade",
|
||||
input: true,
|
||||
secureTextEntry: true,
|
||||
inputPlaceholder: strings.enterAccountPassword(),
|
||||
positiveText: strings.delete(),
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
if (!value || !value.trim()) {
|
||||
ToastManager.error(
|
||||
new Error(strings.passwordNotEntered()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const verified = await db.user?.verifyPassword(value);
|
||||
if (verified) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
startProgress({
|
||||
title: "Deleting account",
|
||||
paragraph:
|
||||
"Please wait while we delete your account"
|
||||
});
|
||||
await db.user?.deleteUser(value);
|
||||
DatabaseLogger.info("User account deleted");
|
||||
Navigation.navigate("Notes");
|
||||
await BiometricService.resetCredentials();
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
} catch (e) {
|
||||
endProgress();
|
||||
DatabaseLogger.error(e);
|
||||
ToastManager.error(
|
||||
e as Error,
|
||||
strings.failedToDeleteAccount(),
|
||||
"global"
|
||||
);
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
ToastManager.error(
|
||||
e as Error,
|
||||
strings.failedToDeleteAccount(),
|
||||
"global"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "sync-settings",
|
||||
name: strings.syncSettings(),
|
||||
description: strings.syncSettingsDesc(),
|
||||
type: "screen",
|
||||
icon: "arrows-clockwise",
|
||||
iconFamily: "notesnook",
|
||||
component: "offline-mode-progress",
|
||||
sections: [
|
||||
{
|
||||
id: "sync-behavior-sub-section",
|
||||
type: "group",
|
||||
name: strings.syncBehavior(),
|
||||
sections: [
|
||||
{
|
||||
id: "auto-sync",
|
||||
name: strings.autoSync(),
|
||||
description: strings.autoSyncDesc(),
|
||||
type: "switch",
|
||||
property: "disableAutoSync",
|
||||
featureId: "syncControls",
|
||||
icon: "arrows-clockwise",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "disable-realtime-sync",
|
||||
name: strings.realtimeSync(),
|
||||
description: strings.realtimeSyncDesc(),
|
||||
type: "switch",
|
||||
property: "disableRealtimeSync",
|
||||
featureId: "syncControls",
|
||||
icon: "cloud-check",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "offline-mode",
|
||||
icon: "wifi-slash",
|
||||
iconFamily: "notesnook",
|
||||
name: strings.fullOfflineMode(),
|
||||
description: strings.fullOfflineModeDesc(),
|
||||
type: "switch",
|
||||
property: "offlineMode",
|
||||
featureId: "fullOfflineMode",
|
||||
modifer: () => {
|
||||
const current = SettingsService.get().offlineMode;
|
||||
if (current) {
|
||||
SettingsService.setProperty("offlineMode", false);
|
||||
db.fs().cancel("offline-mode");
|
||||
return;
|
||||
}
|
||||
SettingsService.setProperty("offlineMode", true);
|
||||
db.attachments.cacheAttachments().catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "advanced-sync-sub-section",
|
||||
type: "group",
|
||||
name: strings.advancedSettings(),
|
||||
sections: [
|
||||
{
|
||||
id: "background-sync",
|
||||
name: strings.backgroundSync(),
|
||||
description: strings.backgroundSyncDesc(),
|
||||
type: "switch",
|
||||
property: "backgroundSync",
|
||||
icon: "arrow-u-up-left",
|
||||
iconFamily: "notesnook",
|
||||
onChange: (value) => {
|
||||
if (value) {
|
||||
BackgroundSync.start();
|
||||
} else {
|
||||
BackgroundSync.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "sync-troubleshooting-sub-section",
|
||||
type: "group",
|
||||
name: strings.troubleshooting(),
|
||||
sections: [
|
||||
{
|
||||
id: "disable-sync",
|
||||
name: strings.pauseSync(),
|
||||
description: strings.pauseSyncDesc(),
|
||||
type: "switch",
|
||||
property: "disableSync",
|
||||
featureId: "syncControls",
|
||||
icon: "pause",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "pull-sync",
|
||||
name: strings.forcePullChanges(),
|
||||
description: strings.forcePullChangesDesc(),
|
||||
icon: "git-pull-request",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
ForceSyncSheet.present("fetch");
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "push-sync",
|
||||
name: strings.forcePushChanges(),
|
||||
description: strings.forcePushChangesDesc(),
|
||||
icon: "arrow-fat-up",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
ForceSyncSheet.present("send");
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "notesnook-circle",
|
||||
name: strings.notesnookCircle(),
|
||||
icon: "users-three",
|
||||
iconFamily: "notesnook",
|
||||
type: "screen",
|
||||
description: strings.notesnookCircleDesc(),
|
||||
component: "notesnook-circle"
|
||||
},
|
||||
{
|
||||
id: "inbox-api",
|
||||
name: strings.inboxAPI(),
|
||||
icon: "inbox",
|
||||
type: "screen",
|
||||
description: strings.inboxAPIDesc(),
|
||||
sections: [
|
||||
{
|
||||
id: "inbox-api-group",
|
||||
name: strings.inboxAPI(),
|
||||
type: "group",
|
||||
sections: [
|
||||
{
|
||||
id: "toggle-inbox-api",
|
||||
name: strings.enableInboxAPI(),
|
||||
description: strings.enableInboxAPIDesc(),
|
||||
type: "switch",
|
||||
icon: "file-cloud",
|
||||
iconFamily: "notesnook",
|
||||
useHook: () => {
|
||||
return useSettingStore((state) => state.inboxEnabled);
|
||||
},
|
||||
getter: (current) => current,
|
||||
modifer: async (current) => {
|
||||
if (current) {
|
||||
return new Promise((resolve) => {
|
||||
presentDialog({
|
||||
title: strings.disableInboxAPI(),
|
||||
paragraph: strings.disableInboxAPIDesc(),
|
||||
positiveText: strings.disable(),
|
||||
onClose: () => {
|
||||
resolve();
|
||||
},
|
||||
positivePress: async () => {
|
||||
try {
|
||||
await db.inboxItemsHistory.deleteFailed();
|
||||
await db.user.discardInboxKeys();
|
||||
useSettingStore.setState({
|
||||
inboxEnabled: false
|
||||
});
|
||||
resolve();
|
||||
return true;
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
message: (e as Error).message,
|
||||
context: "local"
|
||||
});
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
Navigation.push("SettingsGroup", {
|
||||
id: "setup-inbox-keys",
|
||||
name: strings.setupInboxKeys(),
|
||||
type: "screen",
|
||||
component: "setup-inbox-keys"
|
||||
} as any);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "manage-inbox-keys",
|
||||
name: strings.manageInboxKeys(),
|
||||
icon: "tray-arrow-down",
|
||||
iconFamily: "notesnook",
|
||||
useHook: () => useSettingStore((state) => state.inboxEnabled),
|
||||
hidden: (current) => !current,
|
||||
description: strings.manageInboxKeysDesc(),
|
||||
onVerify: async () => {
|
||||
return new Promise((resolve) => {
|
||||
verifyUser(
|
||||
"global",
|
||||
() => {
|
||||
resolve(true);
|
||||
},
|
||||
false,
|
||||
() => resolve(false)
|
||||
);
|
||||
});
|
||||
},
|
||||
type: "screen",
|
||||
component: "manage-inbox-keys"
|
||||
},
|
||||
{
|
||||
id: "inbox-keys",
|
||||
name: strings.viewAPIKeys(),
|
||||
description: strings.viewAPIKeysDesc(),
|
||||
useHook: () => useSettingStore((state) => state.inboxEnabled),
|
||||
hidden: (current) => !current,
|
||||
type: "screen",
|
||||
component: "inbox-keys",
|
||||
icon: "key",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "failed-inbox-items",
|
||||
name: strings.failedInboxItems(),
|
||||
description: strings.failedInboxItemsDesc(),
|
||||
useHook: () => useSettingStore((state) => state.inboxEnabled),
|
||||
hidden: (current) => !current,
|
||||
type: "screen",
|
||||
component: "failed-inbox-items",
|
||||
icon: "warning-circle",
|
||||
iconFamily: "notesnook",
|
||||
hideHeader: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
313
apps/mobile/app/screens/settings/groups/back-restore.ts
Normal file
313
apps/mobile/app/screens/settings/groups/back-restore.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
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 { Platform } from "react-native";
|
||||
import ExportNotesSheet from "../../../components/sheets/export-notes";
|
||||
import BackupService from "../../../services/backup";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { verifyUser } from "../verify-user";
|
||||
import { SettingSection } from "../types";
|
||||
import { restoreBackup } from "../restore-backup";
|
||||
import { keepLocalCopy, pick } from "@react-native-documents/picker";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
|
||||
export const backRestoreGroup: SettingSection = {
|
||||
id: "back-restore",
|
||||
name: strings.backupRestore(),
|
||||
sections: [
|
||||
{
|
||||
id: "backups",
|
||||
type: "screen",
|
||||
name: strings.backups(),
|
||||
icon: "clock-counter-clockwise",
|
||||
iconFamily: "notesnook",
|
||||
description: strings.backupsDesc(),
|
||||
sections: [
|
||||
{
|
||||
id: "backup-actions-group",
|
||||
type: "group",
|
||||
name: strings.backupActions(),
|
||||
sections: [
|
||||
{
|
||||
id: "backup-now",
|
||||
name: strings.backupNow(),
|
||||
description: strings.backupNowDesc(),
|
||||
icon: "clock-counter-clockwise",
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
if (!user || SettingsService.getProperty("encryptedBackup")) {
|
||||
await BackupService.run(true);
|
||||
return;
|
||||
}
|
||||
|
||||
verifyUser(undefined, () => BackupService.run(true));
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "backup-now-with-attachments",
|
||||
name: strings.fullBackup(),
|
||||
description: strings.backupNowWithAttachmentsDesc(),
|
||||
icon: "cloud-check",
|
||||
iconFamily: "notesnook",
|
||||
hidden: () => !useUserStore.getState().user,
|
||||
modifer: async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
if (!user || SettingsService.getProperty("encryptedBackup")) {
|
||||
await BackupService.run(true, undefined, "full");
|
||||
return;
|
||||
}
|
||||
|
||||
verifyUser(undefined, () =>
|
||||
BackupService.run(true, undefined, "full")
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "auto-backups-group",
|
||||
name: strings.automaticBackups(),
|
||||
type: "group",
|
||||
sections: [
|
||||
{
|
||||
id: "auto-backups",
|
||||
type: "component",
|
||||
icon: "clock",
|
||||
iconFamily: "notesnook",
|
||||
name: strings.automaticBackups(),
|
||||
description: strings.automaticBackupsDesc(),
|
||||
component: "autobackups"
|
||||
},
|
||||
{
|
||||
id: "auto-backups-with-attachments",
|
||||
type: "component",
|
||||
icon: "arrows-clockwise",
|
||||
iconFamily: "notesnook",
|
||||
hidden: () => !useUserStore.getState().user,
|
||||
name: strings.automaticFullBackup(),
|
||||
description: [
|
||||
...strings.automaticBackupsWithAttachmentsDesc()
|
||||
].join("\n"),
|
||||
component: "autobackupsattachments"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "storage-location-group",
|
||||
name: strings.storageAndLocation(),
|
||||
type: "group",
|
||||
hidden: () => Platform.OS !== "android",
|
||||
sections: [
|
||||
{
|
||||
id: "select-backup-dir",
|
||||
name: strings.selectBackupDir(),
|
||||
description: () => {
|
||||
const desc = strings.selectBackupDirDesc(
|
||||
SettingsService.get().backupDirectoryAndroid?.path || ""
|
||||
);
|
||||
return desc[0] + " " + desc[1];
|
||||
},
|
||||
icon: "folder",
|
||||
iconFamily: "notesnook",
|
||||
hidden: () =>
|
||||
!!SettingsService.get().backupDirectoryAndroid ||
|
||||
Platform.OS !== "android",
|
||||
property: "backupDirectoryAndroid",
|
||||
modifer: async () => {
|
||||
let dir;
|
||||
try {
|
||||
dir = await BackupService.checkBackupDirExists(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
if (!dir) {
|
||||
ToastManager.show({
|
||||
heading: strings.noDirectorySelected(),
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "change-backup-dir",
|
||||
name: strings.changeBackupDir(),
|
||||
description: () =>
|
||||
SettingsService.get().backupDirectoryAndroid?.name || "",
|
||||
icon: "folder",
|
||||
hidden: () =>
|
||||
!SettingsService.get().backupDirectoryAndroid ||
|
||||
Platform.OS !== "android",
|
||||
property: "backupDirectoryAndroid",
|
||||
modifer: async () => {
|
||||
let dir;
|
||||
try {
|
||||
dir = await BackupService.checkBackupDirExists(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
if (!dir) {
|
||||
ToastManager.show({
|
||||
heading: strings.noDirectorySelected(),
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "backup-encryption-group",
|
||||
type: "group",
|
||||
name: strings.security(),
|
||||
sections: [
|
||||
{
|
||||
id: "enable-backup-encryption",
|
||||
type: "switch",
|
||||
name: strings.backupEncryption(),
|
||||
description: strings.backupEncryptionDesc(),
|
||||
icon: "lock",
|
||||
property: "encryptedBackup",
|
||||
modifer: async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
const settings = SettingsService.get();
|
||||
if (!user) {
|
||||
ToastManager.show({
|
||||
heading: strings.loginRequired(),
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (settings.encryptedBackup) {
|
||||
await verifyUser(undefined, () => {
|
||||
SettingsService.set({
|
||||
encryptedBackup: false
|
||||
});
|
||||
});
|
||||
} else {
|
||||
SettingsService.set({
|
||||
encryptedBackup: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "restore-backup",
|
||||
name: strings.restoreBackup(),
|
||||
description: strings.restoreBackupDesc(),
|
||||
icon: "arrow-counter-clockwise",
|
||||
iconFamily: "notesnook",
|
||||
type: "screen",
|
||||
sections: [
|
||||
{
|
||||
id: "restore-options",
|
||||
type: "group",
|
||||
name: strings.restoreActions(),
|
||||
sections: [
|
||||
{
|
||||
id: "restore-from-files",
|
||||
name: strings.restoreFromFiles(),
|
||||
icon: "folder",
|
||||
modifer: async () => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
const file = await pick();
|
||||
const fileCopy = await keepLocalCopy({
|
||||
destination: "cachesDirectory",
|
||||
files: [
|
||||
{
|
||||
uri: file[0].uri,
|
||||
fileName: file[0].name ?? `backup_restore_${Date.now()}`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (fileCopy[0].status === "error") {
|
||||
ToastManager.error(new Error("File copy error"));
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
restoreBackup({
|
||||
uri: fileCopy[0].localUri,
|
||||
deleteFile: true
|
||||
});
|
||||
},
|
||||
description: strings.selectBackupFileDesc()
|
||||
},
|
||||
{
|
||||
id: "select-backup-folder",
|
||||
name: strings.selectBackupFolder(),
|
||||
icon: "folder",
|
||||
hidden: () => Platform.OS !== "android",
|
||||
modifer: async () => {
|
||||
const folder = await ScopedStorage.openDocumentTree(true);
|
||||
let subfolder;
|
||||
if (folder.name !== "Notesnook backups") {
|
||||
subfolder = await ScopedStorage.createDirectory(
|
||||
folder.uri,
|
||||
"Notesnook backups"
|
||||
);
|
||||
} else {
|
||||
subfolder = folder;
|
||||
}
|
||||
SettingsService.set({
|
||||
backupDirectoryAndroid: subfolder
|
||||
});
|
||||
},
|
||||
description: strings.selectFolderForBackupFilesDesc()
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "recent-backups-group",
|
||||
type: "component",
|
||||
component: "backuprestore"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "export-notes",
|
||||
name: strings.exportAllNotes(),
|
||||
icon: "export",
|
||||
iconFamily: "notesnook",
|
||||
description: strings.exportAllNotesDesc(),
|
||||
modifer: () => {
|
||||
verifyUser(undefined, () => {
|
||||
ExportNotesSheet.present(undefined, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
76
apps/mobile/app/screens/settings/groups/community.ts
Normal file
76
apps/mobile/app/screens/settings/groups/community.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
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 { Linking } from "react-native";
|
||||
import { SettingSection } from "../types";
|
||||
|
||||
export const communityGroup: SettingSection = {
|
||||
id: "community",
|
||||
name: strings.community(),
|
||||
sections: [
|
||||
{
|
||||
id: "join-telegram",
|
||||
name: strings.joinTelegram(),
|
||||
description: strings.joinTelegramDesc(),
|
||||
icon: "telegram-logo",
|
||||
iconFamily: "notesnook",
|
||||
iconSize: 16,
|
||||
modifer: () => {
|
||||
Linking.openURL("https://t.me/notesnook").catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "join-mastodon",
|
||||
name: strings.joinMastodon(),
|
||||
description: strings.joinMastodonDesc(),
|
||||
icon: "mastodon-logo",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
Linking.openURL("https://fosstodon.org/@notesnook").catch(console.log);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "join-twitter",
|
||||
name: strings.followOnX(),
|
||||
description: strings.followOnXDesc(),
|
||||
icon: "x-logo",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
Linking.openURL("https://twitter.com/notesnook").catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "join-discord",
|
||||
name: strings.joinDiscord(),
|
||||
icon: "discord-logo",
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
Linking.openURL("https://discord.gg/zQBK97EE22").catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
},
|
||||
description: strings.joinDiscordDesc()
|
||||
}
|
||||
]
|
||||
};
|
||||
345
apps/mobile/app/screens/settings/groups/customize.ts
Normal file
345
apps/mobile/app/screens/settings/groups/customize.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
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 { Appearance } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import DateFormat from "../../../components/sheets/date-format";
|
||||
import TrashInterval from "../../../components/sheets/trash-interval";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { EDITOR_LINE_HEIGHT } from "../../../utils/constants";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { useDragState } from "../components/editor/state";
|
||||
import { SettingSection } from "../types";
|
||||
|
||||
export const customizeGroup: SettingSection = {
|
||||
id: "customize",
|
||||
name: strings.customization(),
|
||||
sections: [
|
||||
{
|
||||
id: "personalization",
|
||||
type: "screen",
|
||||
name: strings.appearance(),
|
||||
description: strings.appearanceDesc(),
|
||||
icon: "squares-four",
|
||||
iconFamily: "notesnook",
|
||||
sections: [
|
||||
{
|
||||
id: "appearance-group",
|
||||
type: "group",
|
||||
name: strings.appearance(),
|
||||
sections: [
|
||||
{
|
||||
id: "theme-picker",
|
||||
type: "screen",
|
||||
name: strings.themes(),
|
||||
description: strings.themesDesc(),
|
||||
component: "theme-selector",
|
||||
icon: "paint-roller",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "use-system-theme",
|
||||
type: "switch",
|
||||
name: strings.useSystemTheme(),
|
||||
description: strings.useSystemThemeDesc(),
|
||||
property: "useSystemTheme",
|
||||
icon: "swatches",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
const current = SettingsService.get().useSystemTheme;
|
||||
SettingsService.set({
|
||||
useSystemTheme: !current
|
||||
});
|
||||
if (!current) {
|
||||
const systemColorScheme = Appearance.getColorScheme();
|
||||
useThemeStore
|
||||
.getState()
|
||||
.setColorScheme(
|
||||
systemColorScheme === "dark" ? "dark" : "light"
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "enable-dark-mode",
|
||||
type: "switch",
|
||||
name: strings.darkMode(),
|
||||
description: strings.darkModeDesc(),
|
||||
property: "colorScheme",
|
||||
icon: "moon",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
useThemeStore.getState().setColorScheme();
|
||||
},
|
||||
getter: () => useThemeStore.getState().colorScheme === "dark"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "behaviour",
|
||||
type: "screen",
|
||||
icon: "sliders-horizontal",
|
||||
iconFamily: "notesnook",
|
||||
name: strings.behavior(),
|
||||
description: strings.behaviorDesc(),
|
||||
sections: [
|
||||
{
|
||||
id: "navigation-group",
|
||||
type: "group",
|
||||
name: strings.navigation(),
|
||||
sections: [
|
||||
{
|
||||
id: "default-sidebar-view",
|
||||
type: "component",
|
||||
name: strings.defaultSidebarTab(),
|
||||
description: strings.defaultSidebarTabDesc(),
|
||||
component: "sidebar-tab-selector",
|
||||
icon: "table",
|
||||
iconFamily: "notesnook"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "date-time-group",
|
||||
name: strings.dateAndTime(),
|
||||
type: "group",
|
||||
sections: [
|
||||
{
|
||||
id: "date-format",
|
||||
name: strings.dateFormat(),
|
||||
description: strings.dateFormatDesc(),
|
||||
icon: "calendar",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
DateFormat.present();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "day-format",
|
||||
name: strings.dayFormat(),
|
||||
description: strings.dayFormatDesc(),
|
||||
type: "component",
|
||||
component: "day-format-selector",
|
||||
icon: "calendar-check",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "week-format",
|
||||
name: strings.weekFormat(),
|
||||
description: strings.weekFormatDesc(),
|
||||
type: "component",
|
||||
component: "week-format-selector",
|
||||
icon: "calendar-dots",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "time-format",
|
||||
name: strings.timeFormat(),
|
||||
description: strings.timeFormatDesc(),
|
||||
type: "component",
|
||||
component: "time-format-selector",
|
||||
icon: "clock",
|
||||
iconFamily: "notesnook"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "miscellaneous-group",
|
||||
type: "group",
|
||||
name: strings.miscellaneous(),
|
||||
sections: [
|
||||
{
|
||||
id: "clear-trash-interval",
|
||||
name: strings.clearTrashInterval(),
|
||||
description: strings.clearTrashIntervalDesc(),
|
||||
icon: "trash",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
TrashInterval.present();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "image-compression",
|
||||
type: "component",
|
||||
name: strings.imageCompression(),
|
||||
description: strings.imageCompressionDesc(),
|
||||
component: "image-compression-picker",
|
||||
icon: "arrows-clockwise",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "default-notebook",
|
||||
name: strings.clearDefaultNotebook(),
|
||||
description: strings.clearDefaultNotebookDesc(),
|
||||
modifer: () => {
|
||||
db.settings.setDefaultNotebook(undefined);
|
||||
ToastManager.show({
|
||||
heading: strings.defaultNotebookCleared(),
|
||||
type: "success"
|
||||
});
|
||||
},
|
||||
hidden: () => !db.settings.getDefaultNotebook(),
|
||||
icon: "bookmark",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "disable-update-check",
|
||||
type: "switch",
|
||||
name: strings.autoUpdateCheck(),
|
||||
description: strings.autoUpdateCheckDesc(),
|
||||
property: "checkForUpdates",
|
||||
icon: "arrow-clockwise",
|
||||
iconFamily: "notesnook"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "editor",
|
||||
name: strings.editor(),
|
||||
type: "screen",
|
||||
icon: "pencil-simple-line",
|
||||
iconFamily: "notesnook",
|
||||
description: strings.editorDesc(),
|
||||
sections: [
|
||||
{
|
||||
id: "toolbar-group",
|
||||
name: strings.toolbar(),
|
||||
type: "group",
|
||||
sections: [
|
||||
{
|
||||
id: "configure-toolbar",
|
||||
type: "screen",
|
||||
name: strings.customizeToolbar(),
|
||||
description: strings.customizeToolbarDesc(),
|
||||
component: "configuretoolbar"
|
||||
},
|
||||
{
|
||||
id: "reset-toolbar",
|
||||
name: strings.resetToolbar(),
|
||||
description: strings.resetToolbarDesc(),
|
||||
modifer: () => {
|
||||
useDragState.getState().setPreset("default");
|
||||
ToastManager.show({
|
||||
heading: strings.toolbarReset(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "typography-group",
|
||||
name: strings.typography(),
|
||||
type: "group",
|
||||
sections: [
|
||||
{
|
||||
id: "default-font-family",
|
||||
name: strings.defaultFontFamily(),
|
||||
description: strings.defaultFontFamilyDesc(),
|
||||
type: "component",
|
||||
icon: "format-font",
|
||||
property: "defaultFontFamily",
|
||||
component: "font-selector"
|
||||
},
|
||||
{
|
||||
id: "default-font-size",
|
||||
name: strings.defaultFontSize(),
|
||||
description: strings.defaultFontSizeDesc(),
|
||||
type: "input-selector",
|
||||
minInputValue: 8,
|
||||
maxInputValue: 120,
|
||||
inputBadgeValue: "px",
|
||||
icon: "format-size",
|
||||
property: "defaultFontSize"
|
||||
},
|
||||
{
|
||||
id: "default-line-height",
|
||||
name: strings.lineHeight(),
|
||||
description: strings.lineHeightDesc(),
|
||||
type: "input-selector",
|
||||
property: "defaultLineHeight",
|
||||
icon: "format-line-spacing",
|
||||
minInputValue: EDITOR_LINE_HEIGHT.MIN,
|
||||
maxInputValue: EDITOR_LINE_HEIGHT.MAX
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "formatting-group",
|
||||
name: strings.formatting(),
|
||||
type: "group",
|
||||
sections: [
|
||||
{
|
||||
id: "double-spaced-lines",
|
||||
name: strings.doubleSpacedLines(),
|
||||
description: strings.doubleSpacedLinesDesc(),
|
||||
type: "switch",
|
||||
property: "doubleSpacedLines",
|
||||
icon: "format-line-spacing",
|
||||
onChange: () => {
|
||||
ToastManager.show({
|
||||
heading: strings.lineSpacingChanged(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: "toggle-markdown",
|
||||
name: strings.mardownShortcuts(),
|
||||
property: "markdownShortcuts",
|
||||
description: strings.mardownShortcutsDesc(),
|
||||
type: "switch",
|
||||
featureId: "markdownShortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "note-title-group",
|
||||
type: "group",
|
||||
name: strings.newNoteTitle(),
|
||||
sections: [
|
||||
{
|
||||
id: "title-format",
|
||||
name: strings.titleFormat(),
|
||||
component: "title-format",
|
||||
description: strings.titleFormatDesc(),
|
||||
type: "component"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "servers",
|
||||
type: "screen",
|
||||
name: strings.servers(),
|
||||
description: strings.serversConfigurationDesc(),
|
||||
icon: "hard-drives",
|
||||
iconFamily: "notesnook",
|
||||
component: "server-config"
|
||||
}
|
||||
]
|
||||
};
|
||||
84
apps/mobile/app/screens/settings/groups/help-support.tsx
Normal file
84
apps/mobile/app/screens/settings/groups/help-support.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 { strings } from "@notesnook/intl";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React from "react";
|
||||
import { Linking } from "react-native";
|
||||
import DownloadLogs from "../../../components/sheets/download-logs";
|
||||
import { Issue } from "../../../components/sheets/github/issue";
|
||||
import { ToastManager, presentSheet } from "../../../services/event-manager";
|
||||
import { SettingSection } from "../types";
|
||||
export const helpSupportGroup: SettingSection = {
|
||||
id: "help-support",
|
||||
name: strings.helpAndSupport(),
|
||||
sections: [
|
||||
{
|
||||
id: "report-issue",
|
||||
name: strings.reportAnIssue(),
|
||||
icon: "warning-circle",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
presentSheet({
|
||||
//@ts-ignore Migrate to TS
|
||||
component: <Issue />
|
||||
});
|
||||
},
|
||||
description: strings.reportAnIssueDesc()
|
||||
},
|
||||
{
|
||||
id: "email-support",
|
||||
name: strings.emailSupport(),
|
||||
icon: "envelope-simple",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
Clipboard.setString("support@streetwriters.co");
|
||||
ToastManager.show({
|
||||
heading: strings.emailCopied(),
|
||||
type: "success",
|
||||
icon: "content-copy"
|
||||
});
|
||||
setTimeout(() => {
|
||||
Linking.openURL("mailto:support@streetwriters.co");
|
||||
}, 1000);
|
||||
},
|
||||
description: strings.emailSupportDesc()
|
||||
},
|
||||
{
|
||||
id: "docs-link",
|
||||
name: strings.documentation(),
|
||||
modifer: async () => {
|
||||
Linking.openURL("https://help.notesnook.com/");
|
||||
},
|
||||
description: strings.documentationDesc(),
|
||||
icon: "file-text",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "debugging",
|
||||
name: strings.downloadDebugLogs(),
|
||||
description: strings.downloadDebugLogsDesc(),
|
||||
icon: "bug-droid",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
DownloadLogs.present();
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
48
apps/mobile/app/screens/settings/groups/index.ts
Normal file
48
apps/mobile/app/screens/settings/groups/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
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 { SettingSection } from "../types";
|
||||
import { accountLocalGroup } from "./account-local";
|
||||
import { accountGroup } from "./account";
|
||||
import { customizeGroup } from "./customize";
|
||||
import { privacySecurityGroup } from "./privacy-security";
|
||||
import { backRestoreGroup } from "./back-restore";
|
||||
import { productivityGroup } from "./productivity";
|
||||
import { helpSupportGroup } from "./help-support";
|
||||
import { communityGroup } from "./community";
|
||||
import { legalGroup } from "./legal";
|
||||
import { aboutGroup } from "./about";
|
||||
|
||||
export const useSettingsData = () => {
|
||||
return React.useMemo<SettingSection[]>(
|
||||
() => [
|
||||
accountLocalGroup,
|
||||
accountGroup,
|
||||
customizeGroup,
|
||||
privacySecurityGroup,
|
||||
backRestoreGroup,
|
||||
productivityGroup,
|
||||
helpSupportGroup,
|
||||
communityGroup,
|
||||
legalGroup,
|
||||
aboutGroup
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
66
apps/mobile/app/screens/settings/groups/legal.ts
Normal file
66
apps/mobile/app/screens/settings/groups/legal.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
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 { Linking } from "react-native";
|
||||
import { SettingSection } from "../types";
|
||||
|
||||
export const legalGroup: SettingSection = {
|
||||
id: "legal",
|
||||
name: strings.legal(),
|
||||
sections: [
|
||||
{
|
||||
id: "tos",
|
||||
name: strings.tos(),
|
||||
icon: "bag-simple",
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
try {
|
||||
await Linking.openURL("https://notesnook.com/tos");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
description: strings.tosDesc()
|
||||
},
|
||||
{
|
||||
id: "privacy-policy",
|
||||
name: strings.privacyPolicy(),
|
||||
icon: "shield",
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
try {
|
||||
await Linking.openURL("https://notesnook.com/privacy");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
description: strings.privacyPolicyDesc()
|
||||
},
|
||||
{
|
||||
id: "licenses",
|
||||
name: strings.licenses(),
|
||||
type: "screen",
|
||||
component: "licenses",
|
||||
description: strings.ossLibs(),
|
||||
icon: "file-dashed",
|
||||
iconFamily: "notesnook"
|
||||
}
|
||||
]
|
||||
};
|
||||
374
apps/mobile/app/screens/settings/groups/privacy-security.ts
Normal file
374
apps/mobile/app/screens/settings/groups/privacy-security.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
/*
|
||||
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 { db } from "../../../common/database";
|
||||
import { AppLockPassword } from "../../../components/dialogs/applock-password";
|
||||
import AppLockTimeout from "../../../components/sheets/app-lock-timeout";
|
||||
import LockVaultTimer from "../../../components/sheets/lock-vault-timer";
|
||||
import {
|
||||
VaultStatusType,
|
||||
useVaultStatus
|
||||
} from "../../../hooks/use-vault-status";
|
||||
import BiometricService from "../../../services/biometrics";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import {
|
||||
ToastManager,
|
||||
VaultRequestType,
|
||||
openVault
|
||||
} from "../../../services/event-manager";
|
||||
import { verifyUserWithApplock } from "../verify-user";
|
||||
import { SettingSection } from "../types";
|
||||
|
||||
export const privacySecurityGroup: SettingSection = {
|
||||
id: "privacy-security",
|
||||
name: strings.privacyAndSecurity(),
|
||||
sections: [
|
||||
{
|
||||
id: "marketing-emails",
|
||||
type: "switch",
|
||||
icon: "envelope-simple",
|
||||
iconFamily: "notesnook",
|
||||
name: strings.marketingEmails(),
|
||||
description: strings.marketingEmailsDesc(),
|
||||
modifer: async () => {
|
||||
try {
|
||||
await db.user?.changeMarketingConsent(
|
||||
!useUserStore.getState().user?.marketingConsent
|
||||
);
|
||||
useUserStore.getState().setUser(await db.user?.fetchUser());
|
||||
} catch (e) {
|
||||
ToastManager.error(e as Error);
|
||||
}
|
||||
},
|
||||
getter: (current: any) => current?.marketingConsent,
|
||||
useHook: () => useUserStore((state) => state.user),
|
||||
hidden: (current) => !current
|
||||
},
|
||||
{
|
||||
id: "cors-bypass",
|
||||
type: "input",
|
||||
name: strings.corsBypass(),
|
||||
description: strings.corsBypassDesc(),
|
||||
inputProperties: {
|
||||
defaultValue: "https://cors.notesnook.com",
|
||||
autoCorrect: false,
|
||||
keyboardType: "url"
|
||||
},
|
||||
property: "corsProxy",
|
||||
icon: "network",
|
||||
iconFamily: "notesnook"
|
||||
},
|
||||
{
|
||||
id: "vault",
|
||||
type: "screen",
|
||||
name: strings.vault(),
|
||||
description: strings.vaultDesc(),
|
||||
icon: "key",
|
||||
iconFamily: "notesnook",
|
||||
sections: [
|
||||
{
|
||||
id: "vault-group",
|
||||
name: strings.vault(),
|
||||
type: "group",
|
||||
sections: [
|
||||
{
|
||||
id: "create-vault",
|
||||
name: strings.createVault(),
|
||||
description: strings.createVaultDesc(),
|
||||
icon: "key",
|
||||
iconFamily: "notesnook",
|
||||
useHook: useVaultStatus,
|
||||
hidden: (current) => (current as VaultStatusType)?.exists,
|
||||
modifer: () => {
|
||||
openVault({
|
||||
requestType: VaultRequestType.CreateVault,
|
||||
title: strings.createVault(),
|
||||
buttonTitle: strings.create(),
|
||||
positiveButtonType: "accent"
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "biometric-unlock",
|
||||
type: "switch",
|
||||
name: strings.biometricUnlock(),
|
||||
icon: "fingerprint-simple",
|
||||
iconFamily: "notesnook",
|
||||
useHook: useVaultStatus,
|
||||
description: strings.biometricUnlockDesc(),
|
||||
hidden: (current) => {
|
||||
const _current = current as VaultStatusType;
|
||||
return !_current?.exists || !_current?.isBiometryAvailable;
|
||||
},
|
||||
getter: (current) =>
|
||||
(current as VaultStatusType)?.biometryEnrolled,
|
||||
modifer: (current) => {
|
||||
const _current = current as VaultStatusType;
|
||||
const isRevoking = _current.biometryEnrolled;
|
||||
openVault({
|
||||
requestType: isRevoking
|
||||
? VaultRequestType.RevokeFingerprint
|
||||
: VaultRequestType.EnableFingerprint,
|
||||
title: isRevoking
|
||||
? strings.revokeBiometricUnlock()
|
||||
: strings.enableBiometricUnlock(),
|
||||
buttonTitle: isRevoking ? strings.revoke() : strings.enable(),
|
||||
positiveButtonType: "accent"
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "change-vault-password",
|
||||
useHook: useVaultStatus,
|
||||
name: strings.changeVaultPassword(),
|
||||
icon: "pencil-simple",
|
||||
iconFamily: "notesnook",
|
||||
description: strings.changeVaultPasswordDesc(),
|
||||
hidden: (current) => !(current as VaultStatusType)?.exists,
|
||||
modifer: () =>
|
||||
openVault({
|
||||
requestType: VaultRequestType.ChangePassword,
|
||||
title: strings.changeVaultPassword(),
|
||||
buttonTitle: strings.change(),
|
||||
positiveButtonType: "accent"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "lock-vault-after",
|
||||
useHook: useVaultStatus,
|
||||
name: strings.lockVaultAfter(),
|
||||
description: strings.lockVaultAfterDesc(),
|
||||
hidden: (current) => !(current as VaultStatusType)?.exists,
|
||||
icon: "clock",
|
||||
iconFamily: "notesnook",
|
||||
modifer: () => {
|
||||
LockVaultTimer.present();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "clear-vault",
|
||||
useHook: useVaultStatus,
|
||||
description: strings.clearVaultDesc(),
|
||||
name: strings.clearVault(),
|
||||
icon: "paint-brush-household",
|
||||
iconFamily: "notesnook",
|
||||
hidden: (current) => !(current as VaultStatusType)?.exists,
|
||||
modifer: () => {
|
||||
openVault({
|
||||
requestType: VaultRequestType.ClearVault,
|
||||
title: strings.clearVault() + "?",
|
||||
buttonTitle: strings.clear(),
|
||||
positiveButtonType: "accent",
|
||||
icon: "warning-circle"
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "delete-vault",
|
||||
name: strings.deleteVault(),
|
||||
description: strings.deleteVaultDesc(),
|
||||
icon: "trash",
|
||||
iconFamily: "notesnook",
|
||||
type: "danger",
|
||||
useHook: useVaultStatus,
|
||||
hidden: (current) => !(current as VaultStatusType)?.exists,
|
||||
modifer: () => {
|
||||
openVault({
|
||||
requestType: VaultRequestType.DeleteVault,
|
||||
title: strings.deleteVault() + "?",
|
||||
buttonTitle: strings.delete(),
|
||||
positiveButtonType: "accent",
|
||||
icon: "warning-circle"
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "privacy-mode",
|
||||
type: "switch",
|
||||
icon: "eye-slash",
|
||||
iconFamily: "notesnook",
|
||||
name: strings.privacyMode(),
|
||||
description: strings.privacyModeDesc(),
|
||||
modifer: () => {
|
||||
const settings = SettingsService.get();
|
||||
SettingsService.setPrivacyScreen(!settings.privacyScreen);
|
||||
SettingsService.set({ privacyScreen: !settings.privacyScreen });
|
||||
},
|
||||
property: "privacyScreen"
|
||||
},
|
||||
{
|
||||
id: "app-lock",
|
||||
name: strings.appLock(),
|
||||
type: "screen",
|
||||
description: strings.appLockDesc(),
|
||||
icon: "lock-simple",
|
||||
iconFamily: "notesnook",
|
||||
featureId: "appLock",
|
||||
sections: [
|
||||
{
|
||||
id: "app-lock-group",
|
||||
type: "group",
|
||||
name: strings.appLock(),
|
||||
sections: [
|
||||
{
|
||||
id: "app-lock-mode",
|
||||
name: strings.enableAppLock(),
|
||||
description: strings.appLockDesc(),
|
||||
icon: "lock",
|
||||
iconFamily: "notesnook",
|
||||
type: "switch",
|
||||
property: "appLockEnabled",
|
||||
featureId: "appLock",
|
||||
onChange: (property) => {
|
||||
if (property) {
|
||||
SettingsService.set({
|
||||
privacyScreen: true
|
||||
});
|
||||
SettingsService.setPrivacyScreen(true);
|
||||
}
|
||||
},
|
||||
onVerify: async () => {
|
||||
const verified = (await verifyUserWithApplock()) as boolean;
|
||||
if (!verified) return false;
|
||||
|
||||
if (!SettingsService.getProperty("appLockEnabled")) {
|
||||
if (
|
||||
!SettingsService.getProperty(
|
||||
"appLockHasPasswordSecurity"
|
||||
) &&
|
||||
(await BiometricService.isBiometryAvailable())
|
||||
) {
|
||||
SettingsService.setProperty("biometricsAuthEnabled", true);
|
||||
}
|
||||
|
||||
if (
|
||||
!(await BiometricService.isBiometryAvailable()) &&
|
||||
!SettingsService.getProperty("appLockHasPasswordSecurity")
|
||||
) {
|
||||
AppLockPassword.present("create", true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return verified;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "app-lock-timer",
|
||||
name: strings.appLockTimeout(),
|
||||
description: strings.appLockTimeoutDesc(),
|
||||
icon: "clock",
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
if (await verifyUserWithApplock()) {
|
||||
AppLockTimeout.present();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "app-lock-pass-setup",
|
||||
icon: "numpad",
|
||||
iconFamily: "notesnook",
|
||||
name: strings.setupAppLockPassword(),
|
||||
description: strings.setupAppLockPasswordDesc(),
|
||||
hidden: () => {
|
||||
return !!SettingsService.getProperty(
|
||||
"appLockHasPasswordSecurity"
|
||||
);
|
||||
},
|
||||
onVerify: () => {
|
||||
return verifyUserWithApplock();
|
||||
},
|
||||
property: "appLockHasPasswordSecurity",
|
||||
modifer: () => {
|
||||
AppLockPassword.present("create");
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "app-lock-pass-change",
|
||||
icon: "numpad",
|
||||
iconFamily: "notesnook",
|
||||
name: strings.changeAppLockPassword(),
|
||||
description: strings.changeAppLockPasswordDesc(),
|
||||
hidden: () => {
|
||||
return !SettingsService.getProperty(
|
||||
"appLockHasPasswordSecurity"
|
||||
);
|
||||
},
|
||||
property: "appLockHasPasswordSecurity",
|
||||
modifer: () => {
|
||||
AppLockPassword.present("change");
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "app-lock-pass-remove",
|
||||
name: strings.removeAppLockPassword(),
|
||||
description: strings.removeAppLockPasswordDesc(),
|
||||
hidden: () => {
|
||||
return !SettingsService.getProperty(
|
||||
"appLockHasPasswordSecurity"
|
||||
);
|
||||
},
|
||||
icon: "backspace",
|
||||
iconFamily: "notesnook",
|
||||
property: "appLockHasPasswordSecurity",
|
||||
modifer: () => {
|
||||
AppLockPassword.present("remove");
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "app-lock-fingerprint",
|
||||
name: strings.unlockWithBiometrics(),
|
||||
description: strings.unlockWithBiometricsDesc(),
|
||||
type: "switch",
|
||||
|
||||
property: "biometricsAuthEnabled",
|
||||
onVerify: async () => {
|
||||
const verified = await verifyUserWithApplock();
|
||||
if (!verified) return false;
|
||||
|
||||
if (SettingsService.getProperty("biometricsAuthEnabled")) {
|
||||
if (
|
||||
!SettingsService.getProperty("appLockHasPasswordSecurity")
|
||||
) {
|
||||
SettingsService.setProperty("appLockEnabled", false);
|
||||
ToastManager.show({
|
||||
heading: strings.appLockDisabled(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return verified;
|
||||
},
|
||||
icon: "fingerprint-simple",
|
||||
iconFamily: "notesnook"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
131
apps/mobile/app/screens/settings/groups/productivity.ts
Normal file
131
apps/mobile/app/screens/settings/groups/productivity.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
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 notifee from "@notifee/react-native";
|
||||
import { Platform } from "react-native";
|
||||
import Notifications from "../../../services/notifications";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { SettingSection } from "../types";
|
||||
|
||||
export const productivityGroup: SettingSection = {
|
||||
id: "productivity",
|
||||
name: strings.productivity(),
|
||||
sections: [
|
||||
{
|
||||
id: "notification-notes",
|
||||
type: "switch",
|
||||
name: strings.quickNoteNotification(),
|
||||
description: strings.quickNoteNotificationDesc(),
|
||||
property: "notifNotes",
|
||||
icon: "notification",
|
||||
iconFamily: "notesnook",
|
||||
modifer: async () => {
|
||||
const settings = SettingsService.get();
|
||||
if (settings.notifNotes) {
|
||||
Notifications.unpinQuickNote();
|
||||
} else {
|
||||
Notifications.pinQuickNote();
|
||||
}
|
||||
SettingsService.set({
|
||||
notifNotes: !settings.notifNotes
|
||||
});
|
||||
},
|
||||
hidden: () => Platform.OS !== "android",
|
||||
featureId: "createNoteFromNotificationDrawer"
|
||||
},
|
||||
{
|
||||
id: "reminders",
|
||||
type: "screen",
|
||||
name: strings.reminders(),
|
||||
icon: "bell",
|
||||
iconFamily: "notesnook",
|
||||
description: strings.remindersDesc(),
|
||||
sections: [
|
||||
{
|
||||
id: "reminder-notifications",
|
||||
type: "group",
|
||||
name: strings.notifications(),
|
||||
sections: [
|
||||
{
|
||||
id: "enable-reminders",
|
||||
property: "reminderNotifications",
|
||||
type: "switch",
|
||||
name: strings.reminderNotification(),
|
||||
icon: "bell-outline",
|
||||
onChange: (property) => {
|
||||
if (property) {
|
||||
Notifications.setupReminders();
|
||||
} else {
|
||||
Notifications.clearAllTriggers();
|
||||
}
|
||||
},
|
||||
description: strings.reminderNotificationDesc()
|
||||
},
|
||||
{
|
||||
id: "snooze-time",
|
||||
property: "defaultSnoozeTime",
|
||||
type: "input",
|
||||
icon: "bell-z",
|
||||
iconFamily: "notesnook",
|
||||
name: strings.defaultSnoozeTime(),
|
||||
description: strings.defaultSnoozeTimeDesc(),
|
||||
inputProperties: {
|
||||
keyboardType: "decimal-pad",
|
||||
defaultValue: 5 + "",
|
||||
placeholder: strings.setSnoozeTimePlaceholder(),
|
||||
onSubmitEditing: () => {
|
||||
Notifications.setupReminders();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "reminder-sound-ios",
|
||||
type: "screen",
|
||||
name: strings.changeNotificationSound(),
|
||||
description: strings.changeNotificationSoundDesc(),
|
||||
component: "sound-picker",
|
||||
icon: "speaker-high",
|
||||
iconFamily: "notesnook",
|
||||
hidden: () =>
|
||||
Platform.OS === "ios" ||
|
||||
(Platform.OS === "android" && Platform.Version > 25)
|
||||
},
|
||||
{
|
||||
id: "reminder-sound-android",
|
||||
name: strings.changeNotificationSound(),
|
||||
description: strings.changeNotificationSoundDesc(),
|
||||
icon: "speaker-high",
|
||||
iconFamily: "notesnook",
|
||||
hidden: () =>
|
||||
Platform.OS === "ios" ||
|
||||
(Platform.OS === "android" && Platform.Version < 26),
|
||||
modifer: async () => {
|
||||
const id = await Notifications.getChannelId("urgent");
|
||||
if (id) {
|
||||
await notifee.openNotificationSettings(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -25,16 +25,21 @@ import { Header } from "../../components/header";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { SectionGroup } from "./section-group";
|
||||
import { settingsGroups } from "./settings-data";
|
||||
import { useSettingsData } from "./settings-data";
|
||||
import { RouteParams, SettingSection } from "./types";
|
||||
import SettingsUserSection from "./user-section";
|
||||
import SettingsUserSection from "./components/user-section";
|
||||
import { LegendList } from "@legendapp/list";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { View } from "react-native";
|
||||
|
||||
const keyExtractor = (item: SettingSection) => item.id;
|
||||
|
||||
const Home = ({
|
||||
navigation
|
||||
}: NativeStackScreenProps<RouteParams, "SettingsHome">) => {
|
||||
const { colors } = useThemeColors();
|
||||
const settingsGroups = useSettingsData();
|
||||
useNavigationFocus(navigation, {
|
||||
onFocus: () => {
|
||||
useNavigationStore.getState().setFocusedRouteId("Settings");
|
||||
@@ -43,20 +48,32 @@ const Home = ({
|
||||
focusOnInit: true
|
||||
});
|
||||
|
||||
const renderItem = ({ item }: { item: SettingSection; index: number }) =>
|
||||
const renderItem = ({
|
||||
item,
|
||||
index
|
||||
}: {
|
||||
item: SettingSection;
|
||||
index: number;
|
||||
}) =>
|
||||
item.id === "account" ? (
|
||||
<SettingsUserSection item={item} />
|
||||
) : (
|
||||
<SectionGroup item={item} />
|
||||
<SectionGroup item={item} isLast={!settingsGroups[index + 1]} />
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Header
|
||||
renderedInRoute="Settings"
|
||||
title={strings.routes.Settings()}
|
||||
canGoBack={true}
|
||||
hasSearch={false}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border
|
||||
}}
|
||||
id="Settings"
|
||||
/>
|
||||
<DelayLayout type="settings">
|
||||
@@ -65,9 +82,12 @@ const Home = ({
|
||||
data={settingsGroups}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={{
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
/>
|
||||
</DelayLayout>
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export const Settings = () => {
|
||||
paddingRight: insets.right
|
||||
}}
|
||||
>
|
||||
<ScopedThemeProvider value="list">
|
||||
<ScopedThemeProvider value="base">
|
||||
<SettingsStack.Navigator
|
||||
initialRouteName="SettingsHome"
|
||||
screenListeners={{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,232 +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 React from "react";
|
||||
import { CirclePartner, SubscriptionStatus } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import { useState } from "react";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import AppIcon from "../../components/ui/AppIcon";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Notice } from "../../components/ui/notice";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
|
||||
export const NotesnookCircle = () => {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const isOnTrial =
|
||||
PremiumService.get() &&
|
||||
user?.subscription?.status === SubscriptionStatus.TRIAL;
|
||||
const isFree = !PremiumService.get();
|
||||
const partners = useAsync(db.circle.partners, []);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
{!isFree && !isOnTrial ? null : (
|
||||
<View>
|
||||
<Paragraph>
|
||||
{isFree
|
||||
? strings.freeUserCircleNotice()
|
||||
: strings.trialUserCircleNotice()}
|
||||
</Paragraph>
|
||||
|
||||
{!isOnTrial ? null : (
|
||||
<Button
|
||||
title={strings.upgradePlan()}
|
||||
onPress={() => {
|
||||
Navigation.navigate("PayWall", {
|
||||
canGoBack: true,
|
||||
context: useUserStore.getState().user
|
||||
? "logged-in"
|
||||
: "logged-out"
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{partners.loading ? <ActivityIndicator /> : null}
|
||||
|
||||
{partners.error ? (
|
||||
<Notice type="alert" text={partners.error.message} />
|
||||
) : null}
|
||||
|
||||
{partners.result?.map((item) => (
|
||||
<Partner key={item.id} item={item} available={!isFree && !isOnTrial} />
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const Partner = ({
|
||||
item,
|
||||
available
|
||||
}: {
|
||||
item: CirclePartner;
|
||||
available: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [code, setCode] = useState<string>();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading>{item.name}</Heading>
|
||||
<Image
|
||||
src={item.logoBase64}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Paragraph style={{ textAlign: "justify" }}>
|
||||
{item.longDescription.trim()}
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{item.offerDescription}
|
||||
</Paragraph>
|
||||
|
||||
{available ? (
|
||||
<>
|
||||
{!code ? (
|
||||
<Button
|
||||
type="accent"
|
||||
title={strings.redeemCode()}
|
||||
width="100%"
|
||||
onPress={() => {
|
||||
if (!PremiumService.get()) {
|
||||
Navigation.navigate("PayWall", {
|
||||
canGoBack: true,
|
||||
context: useUserStore.getState().user
|
||||
? "logged-in"
|
||||
: "logged-out"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.circle
|
||||
.redeem(item.id)
|
||||
.then((result) => setCode(result?.code))
|
||||
.catch((e) => ToastManager.error(e));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.secondary.border,
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
Clipboard.setString(code);
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.lg}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{code}
|
||||
</Paragraph>
|
||||
|
||||
<AppIcon name="content-copy" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{item.codeRedeemUrl ? (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (item.codeRedeemUrl) {
|
||||
openLinkInBrowser(
|
||||
item.codeRedeemUrl.replace("{{code}}", code)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xxs}
|
||||
>
|
||||
{strings.clickToDirectlyClaimPromo()}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,176 +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, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Menu, MenuItem } from "react-native-material-menu";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { Dialog } from "../../../components/dialog";
|
||||
import { Pressable } from "../../../components/ui/pressable";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { getColorLinearShade } from "../../../utils/colors";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { verifyUser } from "../functions";
|
||||
|
||||
interface PickerOptions<T, B = any> {
|
||||
getValue: () => B;
|
||||
updateValue: (item: T) => Promise<void>;
|
||||
formatValue: (item: T) => any;
|
||||
compareValue: (current: B, item: T) => boolean;
|
||||
getItemKey: (item: T) => string;
|
||||
options: T[];
|
||||
isFeatureAvailable: () => Promise<boolean>;
|
||||
isOptionAvailable: (item: T) => Promise<boolean>;
|
||||
requiresVerification?: () => boolean;
|
||||
onVerify?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function SettingsPicker<T>({
|
||||
getValue,
|
||||
updateValue,
|
||||
formatValue,
|
||||
compareValue,
|
||||
options,
|
||||
getItemKey,
|
||||
isFeatureAvailable,
|
||||
isOptionAvailable,
|
||||
requiresVerification = () => false,
|
||||
onVerify
|
||||
}: PickerOptions<T>) {
|
||||
const { colors, isDark } = useThemeColors("contextMenu");
|
||||
const menuRef = useRef<any>(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
const [currentValue, setCurrentValue] = useState(getValue());
|
||||
|
||||
const onChange = async (item: T) => {
|
||||
if ((await isFeatureAvailable()) && (await isOptionAvailable(item))) {
|
||||
menuRef.current?.hide();
|
||||
await updateValue(item);
|
||||
setCurrentValue(item);
|
||||
return;
|
||||
}
|
||||
|
||||
menuRef.current?.hide();
|
||||
await updateValue(item);
|
||||
setCurrentValue(item);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={(event) => {
|
||||
setWidth(event.nativeEvent.layout.width);
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
animationDuration={200}
|
||||
style={{
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: colors.primary.background,
|
||||
width: width,
|
||||
marginTop: 60,
|
||||
overflow: "hidden",
|
||||
borderWidth: 0.7,
|
||||
borderColor: getColorLinearShade(
|
||||
colors.primary.background,
|
||||
0.07,
|
||||
isDark
|
||||
)
|
||||
}}
|
||||
onRequestClose={() => {
|
||||
menuRef.current?.hide();
|
||||
}}
|
||||
anchor={
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
if (
|
||||
(onVerify && !(await onVerify())) ||
|
||||
!(await isFeatureAvailable())
|
||||
)
|
||||
return;
|
||||
menuRef.current?.show();
|
||||
}}
|
||||
type="secondary"
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Paragraph>{formatValue(currentValue)}</Paragraph>
|
||||
<Icon
|
||||
color={colors.primary.icon}
|
||||
name="menu-down"
|
||||
size={AppFontSize.md}
|
||||
/>
|
||||
</Pressable>
|
||||
}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
|
||||
{options.map((item) => (
|
||||
<MenuItem
|
||||
key={getItemKey(item)}
|
||||
onPress={async () => {
|
||||
if (requiresVerification?.()) {
|
||||
verifyUser("local", () => {
|
||||
onChange(item);
|
||||
});
|
||||
} else {
|
||||
onChange(item);
|
||||
}
|
||||
}}
|
||||
pressColor={colors.primary.hover}
|
||||
style={{
|
||||
backgroundColor: compareValue(currentValue, item)
|
||||
? colors.selected.background
|
||||
: "transparent",
|
||||
width: "100%",
|
||||
maxWidth: width
|
||||
}}
|
||||
textStyle={{
|
||||
fontSize: AppFontSize.sm,
|
||||
fontFamily: "Inter-Regular",
|
||||
color: compareValue(currentValue, item)
|
||||
? colors.primary.accent
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
>
|
||||
{formatValue(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function createSettingsPicker<T, B>(props: PickerOptions<T, B>) {
|
||||
const Selector = () => {
|
||||
return <SettingsPicker {...props} />;
|
||||
};
|
||||
return Selector;
|
||||
}
|
||||
@@ -22,9 +22,8 @@ import { formatBytes, getFormattedDate } from "@notesnook/common";
|
||||
import { LegacyBackupFile } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { keepLocalCopy, pick } from "@react-native-documents/picker";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import RNFetchBlob, { ReactNativeBlobUtilStat } from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { unzip } from "react-native-zip-archive";
|
||||
@@ -39,16 +38,15 @@ import {
|
||||
updateProgress
|
||||
} from "../../../components/dialogs/progress";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import Heading from "../../../components/ui/typography/heading";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { SectionItem } from "../../../screens/settings/section-item";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { refreshAllStores } from "../../../stores/create-db-collection-store";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import Heading from "../../../components/ui/typography/heading";
|
||||
import { Spacing } from "../../../common/design/spacing";
|
||||
|
||||
type PasswordOrKey = { password?: string; encryptionKey?: string };
|
||||
|
||||
@@ -84,7 +82,7 @@ const withPassword = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const restoreBackup = async (options: {
|
||||
export const restoreBackup = async (options: {
|
||||
uri: string;
|
||||
deleteFile?: boolean;
|
||||
}) => {
|
||||
@@ -103,6 +101,7 @@ const restoreBackup = async (options: {
|
||||
startProgress({
|
||||
title: strings.restoring(),
|
||||
paragraph: strings.preparingBackupRestore(),
|
||||
icon: "arrows-clockwise",
|
||||
canHideProgress: false
|
||||
});
|
||||
|
||||
@@ -292,8 +291,9 @@ export const RestoreBackup = () => {
|
||||
BACKUP_FILES_CACHE
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [backupDirectoryAndroid, setBackupDirectoryAndroid] =
|
||||
useState<ScopedStorage.FileType>();
|
||||
const backupDirectoryAndroid = useSettingStore(
|
||||
(state) => state.settings.backupDirectoryAndroid
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
@@ -305,10 +305,8 @@ export const RestoreBackup = () => {
|
||||
try {
|
||||
let files: (ReactNativeBlobUtilStat | ScopedStorage.FileType)[] = [];
|
||||
if (Platform.OS === "android") {
|
||||
const backupDirectory = SettingsService.get().backupDirectoryAndroid;
|
||||
if (backupDirectory) {
|
||||
setBackupDirectoryAndroid(backupDirectory);
|
||||
files = await ScopedStorage.listFiles(backupDirectory.uri);
|
||||
if (backupDirectoryAndroid) {
|
||||
files = await ScopedStorage.listFiles(backupDirectoryAndroid.uri);
|
||||
} else {
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -355,148 +353,72 @@ export const RestoreBackup = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView
|
||||
<LegendList
|
||||
ListHeaderComponent={
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: Spacing.LEVEL_2
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.sm}
|
||||
fontFamily="MEDIUM"
|
||||
>
|
||||
{strings.recentBackups()}
|
||||
</Heading>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
loading ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 300,
|
||||
paddingHorizontal: 50
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
size={AppFontSize.lg}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
height: 300,
|
||||
paddingHorizontal: 50
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.noBackupsFound()}.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
keyExtractor={(item) =>
|
||||
(item as ScopedStorage.FileType).name ||
|
||||
(item as ReactNativeBlobUtilStat).filename
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 200
|
||||
}}
|
||||
/>
|
||||
}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<SectionItem
|
||||
item={{
|
||||
id: "restore-from-files",
|
||||
name: strings.restoreFromFiles(),
|
||||
icon: "folder",
|
||||
modifer: async () => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
const file = await pick();
|
||||
const fileCopy = await keepLocalCopy({
|
||||
destination: "cachesDirectory",
|
||||
files: [
|
||||
{
|
||||
uri: file[0].uri,
|
||||
fileName: file[0].name ?? `backup_restore_${Date.now()}`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (fileCopy[0].status === "error") {
|
||||
ToastManager.error(new Error("File copy error"));
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
restoreBackup({
|
||||
uri: fileCopy[0].localUri,
|
||||
deleteFile: true
|
||||
});
|
||||
},
|
||||
description: strings.selectBackupFileDesc()
|
||||
}}
|
||||
/>
|
||||
|
||||
{Platform.OS === "android" ? (
|
||||
<SectionItem
|
||||
item={{
|
||||
id: "select-backup-folder",
|
||||
name: strings.selectBackupFolder(),
|
||||
icon: "folder",
|
||||
modifer: async () => {
|
||||
const folder = await ScopedStorage.openDocumentTree(true);
|
||||
let subfolder;
|
||||
if (folder.name !== "Notesnook backups") {
|
||||
subfolder = await ScopedStorage.createDirectory(
|
||||
folder.uri,
|
||||
"Notesnook backups"
|
||||
);
|
||||
} else {
|
||||
subfolder = folder;
|
||||
}
|
||||
SettingsService.set({
|
||||
backupDirectoryAndroid: subfolder
|
||||
});
|
||||
setBackupDirectoryAndroid(subfolder);
|
||||
setLoading(true);
|
||||
checkBackups();
|
||||
},
|
||||
description: strings.selectFolderForBackupFilesDesc()
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<LegendList
|
||||
ListHeaderComponent={
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.primary.accent} size={AppFontSize.xs}>
|
||||
{strings.recentBackups()}
|
||||
</Heading>
|
||||
</View>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
loading ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 300,
|
||||
paddingHorizontal: 50
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
size={AppFontSize.lg}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
height: 300,
|
||||
paddingHorizontal: 50
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.noBackupsFound()}.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
keyExtractor={(item) =>
|
||||
(item as ScopedStorage.FileType).name ||
|
||||
(item as ReactNativeBlobUtilStat).filename
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 200
|
||||
}}
|
||||
/>
|
||||
}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
data={files}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</ScrollView>
|
||||
data={files}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -528,35 +450,36 @@ const BackupItem = ({
|
||||
width: "100%",
|
||||
borderRadius: 0,
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: colors.primary.border,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
paddingVertical: Spacing.LEVEL_2,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.separator
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
flexShrink: 1,
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>{itemName}</Paragraph>
|
||||
<Heading size={AppFontSize.md}>{itemName}</Heading>
|
||||
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ width: "100%", maxWidth: "100%" }}
|
||||
>
|
||||
Created on {getFormattedDate(item?.lastModified, "date-time")}
|
||||
{isLegacyBackup ? "(Legacy backup)" : ""} (
|
||||
{formatBytes((item as ReactNativeBlobUtilStat).size)})
|
||||
Created: {getFormattedDate(item?.lastModified, "date-time")}
|
||||
{" • "}
|
||||
{formatBytes((item as ReactNativeBlobUtilStat).size)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<Button
|
||||
title="Restore"
|
||||
type="secondaryAccented"
|
||||
type="plain-outline"
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingVertical: Spacing.LEVEL_1
|
||||
}}
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
|
||||
@@ -17,39 +17,68 @@ 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 } from "react-native";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { SectionItem } from "./section-item";
|
||||
import { SettingSection } from "./types";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
export const SectionGroup = ({ item }: { item: SettingSection }) => {
|
||||
export const SectionGroup = ({
|
||||
item,
|
||||
isLast
|
||||
}: {
|
||||
item: SettingSection;
|
||||
isLast?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const current = item.useHook && item.useHook();
|
||||
const isHidden = item.hidden && item.hidden(current);
|
||||
return isHidden ? null : (
|
||||
<View
|
||||
style={{
|
||||
marginVertical: item.sections ? 10 : 0
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
{item.name && item.sections ? (
|
||||
<Heading
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
marginBottom: Spacing.LEVEL_2
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.sm}
|
||||
fontFamily="MEDIUM"
|
||||
>
|
||||
{(item.name as string).toUpperCase()}
|
||||
{item.name as string}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{item.sections?.map((item) => (
|
||||
<SectionItem key={item.name as string} item={item} />
|
||||
))}
|
||||
{item.sections?.map((sectionItem, index) =>
|
||||
sectionItem.type === "group" ? (
|
||||
<SectionGroup
|
||||
key={sectionItem.id}
|
||||
item={sectionItem}
|
||||
isLast={!item.sections?.[index + 1]}
|
||||
/>
|
||||
) : (
|
||||
<SectionItem key={sectionItem.id as string} item={sectionItem} />
|
||||
)
|
||||
)}
|
||||
|
||||
{isLast ? null : (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.border
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { FeatureResult, useIsFeatureAvailable } from "@notesnook/common";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import {
|
||||
NavigationProp,
|
||||
@@ -25,24 +26,22 @@ import {
|
||||
} from "@react-navigation/native";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, TextInput, View } from "react-native";
|
||||
import { FeatureResult, useIsFeatureAvailable } from "@notesnook/common";
|
||||
//@ts-ignore
|
||||
import ToggleSwitch from "toggle-switch-react-native";
|
||||
import { FontFamily } from "../../common/design/font";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
import PaywallSheet from "../../components/sheets/paywall";
|
||||
import AppIcon from "../../components/ui/AppIcon";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import Input from "../../components/ui/input";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
import Seperator from "../../components/ui/seperator";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import SettingsService from "../../services/settings";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { SettingStore, useSettingStore } from "../../stores/use-setting-store";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { components } from "./components";
|
||||
import { components } from "./components/components";
|
||||
import { RouteParams, SettingSection } from "./types";
|
||||
import { planToDisplayNameShort } from "../../utils/constants";
|
||||
|
||||
const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -67,6 +66,15 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputSelectorValue, setInputSelectorValue] = useState(() =>
|
||||
item.property
|
||||
? `${
|
||||
SettingsService.get()[
|
||||
item.property as keyof SettingStore["settings"]
|
||||
] ?? ""
|
||||
}`
|
||||
: ""
|
||||
);
|
||||
|
||||
const onChangeSettings = async () => {
|
||||
if (isDisabled) return;
|
||||
@@ -93,31 +101,37 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const styles =
|
||||
item.type === "danger"
|
||||
? {
|
||||
backgroundColor: colors.error.background
|
||||
}
|
||||
: {};
|
||||
|
||||
const updateInput = (value: any) => {
|
||||
inputRef?.current?.setNativeProps({
|
||||
text: value + ""
|
||||
});
|
||||
setInputSelectorValue(`${value}`);
|
||||
};
|
||||
|
||||
const onChangeInputSelectorValue = (text: any) => {
|
||||
if (text) {
|
||||
const min = item.minInputValue || 0;
|
||||
const max = item.maxInputValue || 0;
|
||||
const value = parseInt(text);
|
||||
text =
|
||||
Number.isNaN(value) || value < min ? min : value > max ? max : text;
|
||||
|
||||
SettingsService.set({
|
||||
[item.property as string]: `${text}`
|
||||
});
|
||||
const onChangeInputSelectorValue = (text: any, commit?: boolean) => {
|
||||
// While typing (commit === false) an empty field is allowed so the user can
|
||||
// clear and retype; on commit (submit/blur) an empty field falls back to min.
|
||||
if (!text && !commit) {
|
||||
setInputSelectorValue("");
|
||||
return;
|
||||
}
|
||||
|
||||
const min = item.minInputValue || 0;
|
||||
const max = item.maxInputValue || 0;
|
||||
const value = parseInt(text);
|
||||
|
||||
// Always cap the upper bound. Only enforce the lower bound on commit so that
|
||||
// partial entries (e.g. typing "5" on the way to "50" when min is 8) aren't
|
||||
// snapped up prematurely.
|
||||
let clamped: number;
|
||||
if (Number.isNaN(value)) clamped = min;
|
||||
else if (value > max) clamped = max;
|
||||
else if (commit && value < min) clamped = min;
|
||||
else clamped = value;
|
||||
|
||||
// The input is controlled via `value`, so the clamped result is reflected in
|
||||
// the field directly and can never display a value outside the range.
|
||||
setInputSelectorValue(`${clamped}`);
|
||||
SettingsService.set({
|
||||
[item.property as string]: `${clamped}`
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -138,19 +152,24 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
return true;
|
||||
}, [isFeatureAvailable]);
|
||||
|
||||
const isOn = item.getter
|
||||
? item.getter(item.property || current)
|
||||
: settings[item?.property as never];
|
||||
|
||||
return isHidden ? null : (
|
||||
<Pressable
|
||||
disabled={item.type === "component"}
|
||||
style={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
padding: DefaultAppStyles.GAP,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: DefaultAppStyles.GAP,
|
||||
borderRadius: 0,
|
||||
overflow: "hidden",
|
||||
...styles
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingVertical: item.component && !item.name ? 0 : Spacing.LEVEL_1,
|
||||
borderRadius: Radius.S,
|
||||
backgroundColor: colors.primary.background,
|
||||
marginBottom: Spacing.LEVEL_0,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!checkIsFeatureAvailable()) return;
|
||||
@@ -177,101 +196,160 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!isFeatureAvailable?.isAllowed ? (
|
||||
<View
|
||||
style={{
|
||||
width: 35,
|
||||
height: 35,
|
||||
borderRadius: 100,
|
||||
backgroundColor: colors.primary.accent,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
bottom: -8,
|
||||
right: -8
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.static.orange}
|
||||
size={AppFontSize.md}
|
||||
name="crown"
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexShrink: 1
|
||||
flexDirection: "column",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: "center",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginRight: 12,
|
||||
backgroundColor:
|
||||
item.component === "colorpicker"
|
||||
? colors.primary.accent
|
||||
: undefined,
|
||||
borderRadius: 100
|
||||
gap: Spacing.LEVEL_2,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{!!item.icon && (
|
||||
{item.icon ? (
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor:
|
||||
item.component === "colorpicker"
|
||||
? colors.primary.accent
|
||||
: colors.secondary.background,
|
||||
borderRadius: Radius.XS
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={
|
||||
item.type === "danger"
|
||||
? colors.error.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
iconFamily={item.iconFamily}
|
||||
name={item.icon}
|
||||
size={item.iconSize || 16}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingRight: item.type === "switch" ? Spacing.LEVEL_1 : 0,
|
||||
gap: Spacing.LEVEL_1,
|
||||
flexShrink: 1,
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_1,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{item.name ? (
|
||||
<Heading
|
||||
color={colors.primary.heading}
|
||||
fontSize="MD"
|
||||
lineHeight="100%"
|
||||
>
|
||||
{typeof item.name === "function"
|
||||
? item.name(current)
|
||||
: item.name}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{!isFeatureAvailable?.isAllowed ? (
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: Spacing.LEVEL_0 / 2,
|
||||
paddingHorizontal: Spacing.LEVEL_1,
|
||||
borderRadius: 100,
|
||||
backgroundColor: colors.primary.accent,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_0
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.static.orange}
|
||||
size={AppFontSize.md}
|
||||
name="crown"
|
||||
/>
|
||||
{isFeatureAvailable?.availableOn ? (
|
||||
<Paragraph
|
||||
style={{ color: colors.primary.accentForeground }}
|
||||
fontSize="XXS"
|
||||
fontFamily="MEDIUM"
|
||||
>
|
||||
{planToDisplayNameShort(isFeatureAvailable?.availableOn)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{item.description ? (
|
||||
<Paragraph
|
||||
color={colors.primary.paragraph}
|
||||
fontSize="SM"
|
||||
lineHeight="100%"
|
||||
>
|
||||
{typeof item.description === "function"
|
||||
? item.description(current)
|
||||
: item.description}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{item.type === "switch" && !loading && (
|
||||
<AppIcon
|
||||
name={isOn ? "toggle-on" : "toggle-off"}
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={
|
||||
item.type === "danger"
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
isOn
|
||||
? [colors.primary.accent, colors.primary.background]
|
||||
: [colors.disabled.icon, colors.primary.background]
|
||||
}
|
||||
iconFamily={item.iconFamily}
|
||||
name={item.icon}
|
||||
size={item.iconSize || 30}
|
||||
/>
|
||||
)}
|
||||
|
||||
{item.type === "screen" ? (
|
||||
<AppIcon
|
||||
name="chevron-right"
|
||||
iconFamily="notesnook"
|
||||
size={16}
|
||||
color={colors.secondary.paragraph}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
<ActivityIndicator size={16} color={colors.primary.accent} />
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
paddingRight: item.type === "switch" ? 10 : 0,
|
||||
flex: item.type === "component" ? 1 : 0
|
||||
gap: Spacing.LEVEL_1,
|
||||
paddingLeft: item.name ? Spacing.LEVEL_2 + 32 : undefined
|
||||
}}
|
||||
>
|
||||
{item.name ? (
|
||||
<Heading
|
||||
color={
|
||||
item.type === "danger"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.heading
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
>
|
||||
{typeof item.name === "function" ? item.name(current) : item.name}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{!!item.description && (
|
||||
<Paragraph
|
||||
color={
|
||||
item.type === "danger"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
>
|
||||
{typeof item.description === "function"
|
||||
? item.description(current)
|
||||
: item.description}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{!!item.component && item.type !== "screen" && (
|
||||
<>
|
||||
<Seperator half />
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingTop: item.icon ? Spacing.LEVEL_2 : 0
|
||||
}}
|
||||
>
|
||||
{components[item.component]}
|
||||
</>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{item.type === "input" && (
|
||||
@@ -290,7 +368,15 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
});
|
||||
item.inputProperties?.onSubmitEditing?.(text as any);
|
||||
}}
|
||||
containerStyle={{ marginTop: DefaultAppStyles.GAP_VERTICAL }}
|
||||
containerStyle={{
|
||||
marginTop: Spacing.LEVEL_2,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderWidth: 0
|
||||
}}
|
||||
inputStyle={{
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
fontSize={AppFontSize.sm}
|
||||
fwdRef={inputRef}
|
||||
onLayout={() => {
|
||||
inputRef?.current?.setNativeProps({
|
||||
@@ -309,11 +395,111 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
marginTop: Spacing.LEVEL_2,
|
||||
backgroundColor: colors.secondary.background,
|
||||
alignSelf: "flex-start",
|
||||
padding: Spacing.LEVEL_2,
|
||||
gap: Spacing.LEVEL_1,
|
||||
borderRadius: Radius.S
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="plus"
|
||||
color={colors.primary.icon}
|
||||
iconFamily="notesnook"
|
||||
onPress={() => {
|
||||
if (!checkIsFeatureAvailable()) return;
|
||||
if (isDisabled) return;
|
||||
const rawValue = SettingsService.get()[
|
||||
item.property as keyof SettingStore["settings"]
|
||||
] as string;
|
||||
if (rawValue) {
|
||||
const currentValue = parseInt(rawValue);
|
||||
const max = item.maxInputValue || 0;
|
||||
if (currentValue >= max) return;
|
||||
const nextValue = currentValue + 1;
|
||||
SettingsService.set({
|
||||
[item.property as string]: nextValue
|
||||
});
|
||||
updateInput(nextValue);
|
||||
}
|
||||
}}
|
||||
size={16}
|
||||
type="tertiary"
|
||||
style={{
|
||||
borderRadius: Radius.XXS,
|
||||
padding: Spacing.LEVEL_0,
|
||||
width: undefined,
|
||||
height: undefined
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-end",
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
{...item.inputProperties}
|
||||
onSubmit={(e) => {
|
||||
onChangeInputSelectorValue(e.nativeEvent.text, true);
|
||||
item.inputProperties?.onSubmitEditing?.(e);
|
||||
}}
|
||||
editable={!isDisabled}
|
||||
value={inputSelectorValue}
|
||||
onChangeText={(text) => {
|
||||
onChangeInputSelectorValue(text);
|
||||
item.inputProperties?.onSubmitEditing?.(text as any);
|
||||
}}
|
||||
keyboardType="decimal-pad"
|
||||
containerStyle={{
|
||||
borderWidth: 0,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
height: 25,
|
||||
borderRadius: 0,
|
||||
minWidth: 25
|
||||
}}
|
||||
fontSize={AppFontSize.sm}
|
||||
inputStyle={{
|
||||
textAlign: "center",
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
fontFamily: FontFamily.SEMI_BOLD,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
wrapperStyle={{
|
||||
flexGrow: 0,
|
||||
marginBottom: 0,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0
|
||||
}}
|
||||
buttons={
|
||||
<>
|
||||
{item.inputBadgeValue ? (
|
||||
<Paragraph
|
||||
fontSize="XXS"
|
||||
style={{
|
||||
marginLeft: 1,
|
||||
marginTop: 2,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
px
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
name="minus"
|
||||
iconFamily="notesnook"
|
||||
color={colors.primary.icon}
|
||||
onPress={() => {
|
||||
if (!checkIsFeatureAvailable()) return;
|
||||
@@ -332,69 +518,21 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
updateInput(nextValue);
|
||||
}
|
||||
}}
|
||||
size={AppFontSize.xl}
|
||||
/>
|
||||
<Input
|
||||
{...item.inputProperties}
|
||||
onSubmit={(e) => {
|
||||
onChangeInputSelectorValue(e.nativeEvent.text);
|
||||
item.inputProperties?.onSubmitEditing?.(e);
|
||||
size={16}
|
||||
type="tertiary"
|
||||
style={{
|
||||
borderRadius: Radius.XXS,
|
||||
padding: Spacing.LEVEL_0,
|
||||
width: undefined,
|
||||
height: undefined
|
||||
}}
|
||||
editable={!isDisabled}
|
||||
onChangeText={(text) => {
|
||||
onChangeInputSelectorValue(text);
|
||||
item.inputProperties?.onSubmitEditing?.(text as any);
|
||||
}}
|
||||
keyboardType="decimal-pad"
|
||||
containerStyle={{
|
||||
width: 60
|
||||
}}
|
||||
inputStyle={{
|
||||
width: 60,
|
||||
textAlign: "center"
|
||||
}}
|
||||
wrapperStyle={{
|
||||
maxWidth: 60,
|
||||
flexGrow: 0,
|
||||
marginBottom: 0,
|
||||
marginHorizontal: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
fwdRef={inputRef}
|
||||
onLayout={() => {
|
||||
if (item.property) {
|
||||
updateInput(SettingsService.get()[item.property]);
|
||||
}
|
||||
}}
|
||||
defaultValue={item.inputProperties?.defaultValue}
|
||||
/>
|
||||
<IconButton
|
||||
name="plus"
|
||||
color={colors.primary.icon}
|
||||
onPress={() => {
|
||||
if (!checkIsFeatureAvailable()) return;
|
||||
if (isDisabled) return;
|
||||
const rawValue = SettingsService.get()[
|
||||
item.property as keyof SettingStore["settings"]
|
||||
] as string;
|
||||
if (rawValue) {
|
||||
const currentValue = parseInt(rawValue);
|
||||
const max = item.maxInputValue || 0;
|
||||
if (currentValue >= max) return;
|
||||
const nextValue = currentValue + 1;
|
||||
SettingsService.set({
|
||||
[item.property as string]: nextValue
|
||||
});
|
||||
updateInput(nextValue);
|
||||
}
|
||||
}}
|
||||
size={AppFontSize.xl}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{item.type === "switch" && !loading && (
|
||||
{/* {item.type === "switch" && !loading && (
|
||||
<ToggleSwitch
|
||||
isOn={
|
||||
item.getter
|
||||
@@ -407,14 +545,7 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
animationSpeed={150}
|
||||
onToggle={onChangeSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
size={AppFontSize.xxl}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
) : null}
|
||||
)} */}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,238 +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 { isServerCompatible } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Input from "../../components/ui/input";
|
||||
import { Notice } from "../../components/ui/notice";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { HostId, HostIds } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const ServerIds = [
|
||||
"notesnook-sync",
|
||||
"auth",
|
||||
"sse",
|
||||
"monograph"
|
||||
] as const;
|
||||
export type ServerId = (typeof ServerIds)[number];
|
||||
type Server = {
|
||||
id: ServerId;
|
||||
host: HostId;
|
||||
title: string;
|
||||
example: string;
|
||||
description: string;
|
||||
versionEndpoint: string;
|
||||
};
|
||||
type VersionResponse = {
|
||||
version: number;
|
||||
id: string;
|
||||
instance: string;
|
||||
};
|
||||
const SERVERS: Server[] = [
|
||||
{
|
||||
id: "notesnook-sync",
|
||||
host: "API_HOST",
|
||||
title: strings.syncServer(),
|
||||
example: "http://localhost:4326",
|
||||
description: strings.syncServerDesc(),
|
||||
versionEndpoint: "/version"
|
||||
},
|
||||
{
|
||||
id: "auth",
|
||||
host: "AUTH_HOST",
|
||||
title: strings.authServer(),
|
||||
example: "http://localhost:5326",
|
||||
description: strings.authServerDesc(),
|
||||
versionEndpoint: "/version"
|
||||
},
|
||||
{
|
||||
id: "sse",
|
||||
host: "SSE_HOST",
|
||||
title: strings.sseServer(),
|
||||
example: "http://localhost:7326",
|
||||
description: strings.sseServerDesc(),
|
||||
versionEndpoint: "/version"
|
||||
},
|
||||
{
|
||||
id: "monograph",
|
||||
host: "MONOGRAPH_HOST",
|
||||
title: strings.monographServer(),
|
||||
example: "http://localhost:6326",
|
||||
description: strings.monographServerDesc(),
|
||||
versionEndpoint: "/api/version"
|
||||
}
|
||||
];
|
||||
export function ServersConfiguration() {
|
||||
const { colors } = useThemeColors();
|
||||
const [error, setError] = useState<string>();
|
||||
const [success, setSuccess] = useState<boolean>();
|
||||
const [urls, setUrls] = useState<Partial<Record<HostId, string>>>(
|
||||
SettingsService.getProperty("serverUrls") || {}
|
||||
);
|
||||
const isLoggedIn = useUserStore((state) => !!state.user);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: 12,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
{isLoggedIn ? (
|
||||
<Notice text={strings.logoutToChangeServerUrls()} type="alert" />
|
||||
) : null}
|
||||
|
||||
<View style={{ flexDirection: "column" }}>
|
||||
{SERVERS.map((server) => (
|
||||
<Input
|
||||
key={server.id}
|
||||
editable={!isLoggedIn}
|
||||
placeholder={`${server.id} e.g. ${server.example}`}
|
||||
validationType="url"
|
||||
defaultValue={urls[server.host]}
|
||||
errorMessage={strings.enterValidUrl()}
|
||||
onChangeText={(value) =>
|
||||
setUrls((s) => {
|
||||
s[server.host] = value;
|
||||
return s;
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{error ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
color={colors.error.paragraph}
|
||||
>
|
||||
{error}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{success === true ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
color={colors.success.paragraph}
|
||||
>
|
||||
{strings.connectedToServer()}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<View
|
||||
style={{
|
||||
marginTop: 1,
|
||||
justifyContent: "flex-end",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
disabled={isLoggedIn}
|
||||
type="secondary"
|
||||
width="100%"
|
||||
onPress={async () => {
|
||||
setError(undefined);
|
||||
try {
|
||||
for (const host of HostIds) {
|
||||
const url = urls[host];
|
||||
const server = SERVERS.find((s) => s.host === host)!;
|
||||
if (!server) throw new Error(strings.serverNotFound(host));
|
||||
if (!url) throw new Error(strings.allServerUrlsRequired());
|
||||
const version = await fetch(`${url}${server.versionEndpoint}`)
|
||||
.then((r) => r.json() as Promise<VersionResponse>)
|
||||
.catch(() => undefined);
|
||||
if (!version)
|
||||
throw new Error(
|
||||
`${strings.couldNotConnectTo(server.title)}`
|
||||
);
|
||||
if (version.id !== server.id)
|
||||
throw new Error(
|
||||
`${strings.incorrectServerUrl(url, server.title)}.`
|
||||
);
|
||||
if (!isServerCompatible(version.version)) {
|
||||
throw new Error(
|
||||
strings.serverVersionMismatch(server.title, url)
|
||||
);
|
||||
}
|
||||
}
|
||||
setSuccess(true);
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
}
|
||||
}}
|
||||
title={strings.testConnection()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="accent"
|
||||
disabled={isLoggedIn}
|
||||
width="100%"
|
||||
onPress={async () => {
|
||||
if (!success) {
|
||||
ToastManager.show({
|
||||
heading: strings.testConnectionBeforeSave()
|
||||
});
|
||||
return;
|
||||
}
|
||||
SettingsService.setProperty(
|
||||
"serverUrls",
|
||||
urls as Record<HostId, string>
|
||||
);
|
||||
|
||||
presentDialog({
|
||||
title: strings.serverUrlChanged(),
|
||||
paragraph: strings.restartAppToTakeEffect(),
|
||||
negativeText: strings.done()
|
||||
});
|
||||
}}
|
||||
title={strings.save()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={isLoggedIn}
|
||||
type="error"
|
||||
width="100%"
|
||||
title={strings.resetServerUrls()}
|
||||
onPress={async () => {
|
||||
if (isLoggedIn) return;
|
||||
SettingsService.setProperty("serverUrls", undefined);
|
||||
presentDialog({
|
||||
title: strings.serverUrlsReset(),
|
||||
paragraph: strings.restartAppToTakeEffect(),
|
||||
negativeText: strings.done()
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user