mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
238abee126 | ||
|
|
a8d0fa3b94 | ||
|
|
27b3cfcd23 | ||
|
|
d4d7eac240 | ||
|
|
e7fb246c25 | ||
|
|
03defb3a48 | ||
|
|
73c44d476b | ||
|
|
0aaabfab62 | ||
|
|
14d680c092 | ||
|
|
28f95b0061 | ||
|
|
419dd555f4 | ||
|
|
1312ce7d50 | ||
|
|
d16cd510b3 | ||
|
|
af08ed5174 | ||
|
|
365e1d7029 | ||
|
|
80b75fc28c | ||
|
|
6de4b4ac2d | ||
|
|
39bfff6b14 | ||
|
|
e381b62e5c | ||
|
|
ffef5affce | ||
|
|
780276b30e | ||
|
|
276239dd2b | ||
|
|
5ef3bdcad4 | ||
|
|
63076d37f5 | ||
|
|
c82487983c | ||
|
|
cf6262e995 | ||
|
|
8a325bf7c0 | ||
|
|
883667b411 |
@@ -22,7 +22,7 @@ import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { db } from "../../common/database";
|
||||
@@ -59,6 +59,7 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { createFormRef, validators } from "../ui/input/form-input";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
@@ -153,50 +154,100 @@ const Actions = ({
|
||||
{
|
||||
name: strings.rename(),
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
input: true,
|
||||
title: strings.renameFile(),
|
||||
defaultValue: attachment.filename,
|
||||
positivePress: async (value) => {
|
||||
if (value && value.length > 0) {
|
||||
await db.attachments.add({
|
||||
hash: attachment.hash,
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
positiveText: strings.rename()
|
||||
});
|
||||
close?.();
|
||||
setTimeout(() => {
|
||||
presentDialog({
|
||||
title: strings.renameFile(),
|
||||
form: {
|
||||
formRef: createFormRef({
|
||||
name: attachment.filename
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
name: "name",
|
||||
defaultValue: attachment.filename,
|
||||
placeholder: strings.enterTitle(),
|
||||
ref: React.createRef<TextInput | null>(),
|
||||
validators: [validators.required(strings.nameIsRequired())]
|
||||
}
|
||||
],
|
||||
onFormSubmit: async (form) => {
|
||||
try {
|
||||
const value = form.getValue("name");
|
||||
await db.attachments.add({
|
||||
hash: attachment.hash,
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
ToastManager.show({
|
||||
message: `Attachment renamed to ${value}`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
form.setError("name", (e as Error).message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
positiveText: strings.rename()
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
icon: "form-textbox"
|
||||
},
|
||||
{
|
||||
name: strings.delete(),
|
||||
onPress: async () => {
|
||||
const relations = await db.relations.to(attachment, "note").get();
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
relations
|
||||
.map((relation) => relation.fromId)
|
||||
.forEach(async (id) => {
|
||||
useTabStore.getState().forEachNoteTab(id, async (tab) => {
|
||||
const isFocused = useTabStore.getState().currentTab === tab.id;
|
||||
if (isFocused) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(id),
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
editorController.current.commands.setLoading(true, tab.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
close?.();
|
||||
setTimeout(() => {
|
||||
presentDialog({
|
||||
title: strings.deleteAttachment(),
|
||||
paragraph: strings.deleteAttachmentConfirm(),
|
||||
positiveText: strings.yes(),
|
||||
negativeText: strings.no(),
|
||||
positiveType: "errorShade",
|
||||
positivePress: async () => {
|
||||
try {
|
||||
const relations = await db.relations
|
||||
.to(attachment, "note")
|
||||
.get();
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
ToastManager.show({
|
||||
type: "success",
|
||||
message: strings.attachmentDeleted()
|
||||
});
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
relations
|
||||
.map((relation) => relation.fromId)
|
||||
.forEach(async (id) => {
|
||||
useTabStore.getState().forEachNoteTab(id, async (tab) => {
|
||||
const isFocused =
|
||||
useTabStore.getState().currentTab === tab.id;
|
||||
if (isFocused) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(id),
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
editorController.current.commands.setLoading(
|
||||
true,
|
||||
tab.id
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
icon: "delete-outline"
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ 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 { db } from "../../common/database";
|
||||
@@ -26,42 +27,44 @@ import { eSendEvent, 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";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
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";
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const password = useRef<string>(undefined);
|
||||
const { colors } = useThemeColors();
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
oldPassword: "",
|
||||
password: ""
|
||||
})
|
||||
);
|
||||
const oldPasswordInputRef = useRef<TextInput>(null);
|
||||
const oldPassword = useRef<string>(undefined);
|
||||
|
||||
const [error, setError] = useState(false);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const user = useUserStore((state) => state.user);
|
||||
|
||||
const changePassword = async () => {
|
||||
setError(undefined);
|
||||
formRef.current.clearErrors();
|
||||
|
||||
if (!user?.isEmailConfirmed) {
|
||||
ToastManager.show({
|
||||
heading: strings.emailNotConfirmed(),
|
||||
message: strings.emailNotConfirmedDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setError(strings.emailNotConfirmedDesc());
|
||||
return;
|
||||
}
|
||||
if (error || !oldPassword.current || !password.current) {
|
||||
ToastManager.show({
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
if (!formRef.current.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = formRef.current.getValues();
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(
|
||||
@@ -74,8 +77,8 @@ export const ChangePassword = () => {
|
||||
}
|
||||
|
||||
const passwordChanged = await db.user.changePassword(
|
||||
oldPassword.current,
|
||||
password.current
|
||||
values.oldPassword,
|
||||
values.password
|
||||
);
|
||||
|
||||
if (!passwordChanged) {
|
||||
@@ -91,15 +94,15 @@ export const ChangePassword = () => {
|
||||
Navigation.goBack();
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
} catch (e) {
|
||||
const message = (e as Error).message;
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.passwordChangeFailed(),
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
if (/old password/i.test(message)) {
|
||||
formRef.current.setError("oldPassword", message);
|
||||
} else {
|
||||
setError(message);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -110,36 +113,61 @@ export const ChangePassword = () => {
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<Input
|
||||
<FormInput
|
||||
name="oldPassword"
|
||||
formRef={formRef}
|
||||
fwdRef={oldPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
oldPassword.current = value;
|
||||
}}
|
||||
loading={loading}
|
||||
validators={[validators.required(strings.currentPasswordRequired())]}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.oldPassword()}
|
||||
placeholder={strings.currentPassword()}
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
loading={loading}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
validationType="password"
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.newPassword()}
|
||||
onSubmitEditing={() => {
|
||||
changePassword();
|
||||
}}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Notice text={strings.changePasswordNotice()} type="alert" />
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
@@ -27,14 +27,15 @@ import useTimer from "../../hooks/use-timer";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
|
||||
type MFAInfo = {
|
||||
primaryMethod: string;
|
||||
@@ -52,7 +53,8 @@ const TwoFactorVerification = ({
|
||||
method: string;
|
||||
code: string;
|
||||
},
|
||||
callback: (result: any) => void
|
||||
callback: (result: any) => void,
|
||||
onerror: (e: Error) => void
|
||||
) => Promise<void>;
|
||||
mfaInfo: MFAInfo;
|
||||
onCancel: () => void;
|
||||
@@ -66,15 +68,26 @@ const TwoFactorVerification = ({
|
||||
method: mfaInfo?.primaryMethod,
|
||||
isPrimary: true
|
||||
});
|
||||
const { seconds, start, reset } = useTimer(currentMethod.method!);
|
||||
const { seconds, start, reset, secondsRef } = useTimer(currentMethod.method!);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
|
||||
const onNext = async () => {
|
||||
if (!code.current || code.current.length < 6 || !currentMethod.method)
|
||||
if (!code.current || code.current.length < 6) {
|
||||
setError(
|
||||
new Error("Please provide a valid multi-factor authentication code.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentMethod.method) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
inputRef.current?.blur();
|
||||
await onMfaLogin(
|
||||
{
|
||||
@@ -86,6 +99,9 @@ const TwoFactorVerification = ({
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
(e) => {
|
||||
setError(e);
|
||||
}
|
||||
);
|
||||
setLoading(false);
|
||||
@@ -131,7 +147,7 @@ const TwoFactorVerification = ({
|
||||
};
|
||||
|
||||
const onSendCode = useCallback(async () => {
|
||||
if (seconds || sending) return;
|
||||
if (secondsRef.current || sending) return;
|
||||
setSending(true);
|
||||
try {
|
||||
await db.mfa.sendCode(currentMethod.method as "sms" | "email");
|
||||
@@ -139,15 +155,18 @@ const TwoFactorVerification = ({
|
||||
setSending(false);
|
||||
} catch (e) {
|
||||
setSending(false);
|
||||
ToastManager.error(e as Error, "Error sending 2FA Code", "local");
|
||||
setError(
|
||||
new Error(`Error sending 2FA Code. Tap "Send code" to try again `)
|
||||
);
|
||||
}
|
||||
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
||||
}, [currentMethod.method, secondsRef, sending, start]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentMethod.method === "sms" || currentMethod.method === "email") {
|
||||
onSendCode();
|
||||
}
|
||||
}, [currentMethod.method, onSendCode]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentMethod.method]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
@@ -234,6 +253,7 @@ const TwoFactorVerification = ({
|
||||
fwdRef={inputRef}
|
||||
textAlign="center"
|
||||
onChangeText={(value) => {
|
||||
setError(undefined);
|
||||
code.current = value;
|
||||
}}
|
||||
cursorColor={colors.selected.accent}
|
||||
@@ -241,6 +261,7 @@ const TwoFactorVerification = ({
|
||||
selectionColor={colors.selected.accent}
|
||||
onSubmitEditing={onNext}
|
||||
height={60}
|
||||
marginBottom={0}
|
||||
inputStyle={{
|
||||
fontSize: AppFontSize.lg,
|
||||
textAlign: "center",
|
||||
@@ -254,10 +275,26 @@ const TwoFactorVerification = ({
|
||||
containerStyle={{
|
||||
minWidth: "50%"
|
||||
}}
|
||||
wrapperStyle={{
|
||||
height: 60
|
||||
}}
|
||||
/>
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
maxWidth: 250
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error?.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
title={loading ? null : strings.next()}
|
||||
@@ -338,7 +375,8 @@ TwoFactorVerification.present = (
|
||||
method: string;
|
||||
code: string;
|
||||
},
|
||||
callback: (result: any) => void
|
||||
callback: (result: any) => void,
|
||||
onerror: (e: Error) => void
|
||||
) => Promise<void>,
|
||||
data: MFAInfo,
|
||||
onCancel: () => void,
|
||||
|
||||
@@ -70,7 +70,7 @@ export const useLogin = (
|
||||
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(
|
||||
async (mfa: any, callback: (success: boolean) => void) => {
|
||||
async (mfa: any, callback: (success: boolean) => void, onerror: (e: Error) => void) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
@@ -92,6 +92,9 @@ export const useLogin = (
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
ToastManager.error(new Error("Token expired, try logging in again"));
|
||||
} else {
|
||||
onerror(e as Error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -136,7 +136,7 @@ const BaseDialog = ({
|
||||
? background
|
||||
: transparent
|
||||
? "transparent"
|
||||
: "rgba(0,0,0,0.3)"
|
||||
: "rgba(0,0,0,0.1)"
|
||||
}}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
|
||||
@@ -20,7 +20,7 @@ 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 { StyleSheet, View } from "react-native";
|
||||
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { getColorLinearShade } from "../../utils/colors";
|
||||
@@ -79,6 +79,11 @@ const DialogButtons = ({
|
||||
/>
|
||||
<Paragraph color={colors.primary.accent}>{" " + doneText}</Paragraph>
|
||||
</View>
|
||||
) : loading ? (
|
||||
<ActivityIndicator
|
||||
size={AppFontSize.lg}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
) : (
|
||||
<View />
|
||||
)}
|
||||
@@ -105,7 +110,6 @@ const DialogButtons = ({
|
||||
style={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
loading={loading}
|
||||
bold
|
||||
type={positiveType || "transparent"}
|
||||
title={positiveTitle}
|
||||
|
||||
@@ -17,10 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { KeyboardTypeOptions } from "react-native";
|
||||
import { KeyboardTypeOptions, TextInput } from "react-native";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { eCloseSimpleDialog, eOpenSimpleDialog } from "../../utils/events";
|
||||
import { ButtonProps } from "../ui/button";
|
||||
import { FieldValidator, FormRef } from "../ui/input/form-input";
|
||||
import { RefObject } from "react";
|
||||
|
||||
export type DialogInfo = {
|
||||
title?: string;
|
||||
@@ -43,6 +45,18 @@ export type DialogInfo = {
|
||||
| "errorShade";
|
||||
icon?: string;
|
||||
paragraphColor: string;
|
||||
form?: {
|
||||
formRef: FormRef;
|
||||
items: {
|
||||
name: string;
|
||||
placeholder: string;
|
||||
label?: string;
|
||||
validators: FieldValidator[];
|
||||
defaultValue?: string;
|
||||
ref: RefObject<TextInput | null>;
|
||||
}[];
|
||||
onFormSubmit?: (form: FormRef) => Promise<boolean>;
|
||||
};
|
||||
input: boolean;
|
||||
inputPlaceholder: string;
|
||||
defaultValue: string;
|
||||
|
||||
@@ -18,7 +18,13 @@ 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 React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
RefObject
|
||||
} from "react";
|
||||
import { TextInput, View, ViewStyle } from "react-native";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
@@ -34,6 +40,7 @@ import { sleep } from "../../utils/time";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { FormInput, type FormRef } from "../ui/input/form-input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Seperator from "../ui/seperator";
|
||||
import BaseDialog from "./base-dialog";
|
||||
@@ -53,15 +60,38 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
});
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [dialogInfo, setDialogInfo] = useState<DialogInfo>();
|
||||
const formRef = useRef(dialogInfo?.form?.formRef);
|
||||
formRef.current = dialogInfo?.form?.formRef;
|
||||
|
||||
const onPressPositive = async () => {
|
||||
if (dialogInfo?.positivePress) {
|
||||
// Handle form submission if form is available
|
||||
if (dialogInfo?.form && formRef.current) {
|
||||
inputRef.current?.blur();
|
||||
try {
|
||||
const isValid = await formRef.current.validate();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
if (dialogInfo.form.onFormSubmit) {
|
||||
setLoading(true);
|
||||
const result = await dialogInfo.form.onFormSubmit(formRef.current);
|
||||
if (result === false) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
/** Empty */
|
||||
}
|
||||
setLoading(false);
|
||||
} else if (dialogInfo?.positivePress) {
|
||||
// Handle old input-based submission
|
||||
inputRef.current?.blur();
|
||||
setLoading(true);
|
||||
let result = false;
|
||||
try {
|
||||
result = await dialogInfo.positivePress(
|
||||
values.current.inputValue || dialogInfo.defaultValue,
|
||||
values.current.inputValue,
|
||||
checked
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -76,6 +106,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
formRef.current = undefined;
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
@@ -85,6 +116,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setChecked(data.check?.defaultValue);
|
||||
formRef.current = data?.form?.formRef;
|
||||
values.current.inputValue = data.defaultValue;
|
||||
setVisible(true);
|
||||
},
|
||||
@@ -94,6 +126,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
const hide = React.useCallback(() => {
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
formRef.current = undefined;
|
||||
setVisible(false);
|
||||
setDialogInfo(undefined);
|
||||
dialogInfo?.onClose?.();
|
||||
@@ -134,19 +167,30 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
? false
|
||||
: dialogInfo.statusBarTranslucent
|
||||
}
|
||||
bounce={!dialogInfo.input}
|
||||
bounce={!dialogInfo.input && !dialogInfo.form}
|
||||
closeOnTouch={!dialogInfo.disableBackdropClosing}
|
||||
background={dialogInfo.background}
|
||||
transparent={
|
||||
dialogInfo.transparent === undefined ? true : dialogInfo.transparent
|
||||
dialogInfo.transparent === undefined ? false : dialogInfo.transparent
|
||||
}
|
||||
onShow={async () => {
|
||||
if (dialogInfo.input) {
|
||||
if (dialogInfo.input && !dialogInfo.form) {
|
||||
inputRef.current?.setNativeProps({
|
||||
text: dialogInfo.defaultValue
|
||||
});
|
||||
await sleep(300);
|
||||
inputRef.current?.focus();
|
||||
} else if (dialogInfo.form) {
|
||||
const items = dialogInfo.form?.items;
|
||||
const firstItem = items[0];
|
||||
for (const item of items) {
|
||||
if (item.defaultValue) {
|
||||
item.ref.current?.setNativeProps({
|
||||
text: dialogInfo.defaultValue
|
||||
});
|
||||
}
|
||||
}
|
||||
firstItem?.ref?.current?.focus();
|
||||
}
|
||||
}}
|
||||
visible={true}
|
||||
@@ -170,7 +214,36 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
{dialogInfo.input ? (
|
||||
{dialogInfo.form ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP / 2
|
||||
}}
|
||||
>
|
||||
{dialogInfo.form.items.map((item, index) => (
|
||||
<FormInput
|
||||
key={item.name}
|
||||
fwdRef={item.ref}
|
||||
name={item.name}
|
||||
autoFocus={index === 0}
|
||||
placeholder={item.placeholder}
|
||||
formRef={formRef as RefObject<FormRef>}
|
||||
validators={item.validators}
|
||||
defaultValue={item.defaultValue}
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
onSubmitEditing={() => {
|
||||
const nextItem = dialogInfo?.form?.items?.[index + 1];
|
||||
if (nextItem) {
|
||||
nextItem?.ref.current?.focus();
|
||||
} else {
|
||||
onPressPositive();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : dialogInfo.input ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
@@ -184,7 +257,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
}}
|
||||
testID="input-value"
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
//defaultValue={dialogInfo.defaultValue}
|
||||
defaultValue={dialogInfo.defaultValue}
|
||||
onSubmit={() => {
|
||||
onPressPositive();
|
||||
}}
|
||||
@@ -237,7 +310,10 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
|
||||
<DialogButtons
|
||||
onPressNegative={onNegativePress}
|
||||
onPressPositive={dialogInfo.positivePress && onPressPositive}
|
||||
onPressPositive={
|
||||
(dialogInfo.positivePress || dialogInfo.form?.onFormSubmit) &&
|
||||
onPressPositive
|
||||
}
|
||||
loading={loading}
|
||||
positiveTitle={dialogInfo.positiveText}
|
||||
negativeTitle={dialogInfo.negativeText}
|
||||
|
||||
@@ -368,7 +368,14 @@ export const useActions = ({
|
||||
inputPlaceholder: strings.name(),
|
||||
defaultValue: item.title,
|
||||
positivePress: async (value) => {
|
||||
if (!value || value.trim().length === 0) return;
|
||||
if (!value || value.trim().length === 0) {
|
||||
ToastManager.error(
|
||||
new Error(strings.nameIsRequired()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
await db.colors.add({
|
||||
id: item.id,
|
||||
title: value
|
||||
|
||||
@@ -95,6 +95,7 @@ import { BETA } from "../utils/constants";
|
||||
import {
|
||||
eAfterSync,
|
||||
eCloseSheet,
|
||||
eCloseSimpleDialog,
|
||||
eEditorReset,
|
||||
eLoginSessionExpired,
|
||||
eOnLoadNote,
|
||||
@@ -286,7 +287,10 @@ const onUserSubscriptionStatusChanged = async (
|
||||
subscription: subscription
|
||||
}
|
||||
});
|
||||
Walkthrough.present("prouser", false, true);
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
setTimeout(() => {
|
||||
Walkthrough.present("prouser", false, true);
|
||||
}, 500);
|
||||
}
|
||||
await PremiumService.setPremiumStatus();
|
||||
useMessageStore.getState().setAnnouncement();
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Plan, SubscriptionPlan } from "@notesnook/core";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import { Platform } from "react-native";
|
||||
import Config from "react-native-config";
|
||||
@@ -181,6 +181,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
const [cancelPromo, setCancelPromo] = useState(false);
|
||||
const [userCanRequestTrial, setUserCanRequestTrial] = useState(false);
|
||||
const [webPricingPlans, setWebPricingPlans] = useState<Plan[]>([]);
|
||||
const plansRef = useRef(plans);
|
||||
plansRef.current = plans;
|
||||
|
||||
const getProduct = React.useCallback(
|
||||
(planId: string, skuId: string) => {
|
||||
@@ -190,11 +192,11 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
);
|
||||
|
||||
return (
|
||||
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
|
||||
plans.find((p) => p.id === planId)?.products?.[skuId]
|
||||
plansRef.current.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
|
||||
plansRef.current.find((p) => p.id === planId)?.products?.[skuId]
|
||||
);
|
||||
},
|
||||
[plans, webPricingPlans]
|
||||
[webPricingPlans]
|
||||
);
|
||||
|
||||
const getProductAndroid = (planId: string, skuId: string) => {
|
||||
@@ -253,35 +255,41 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
|
||||
useEffect(() => {
|
||||
const loadPlans = async () => {
|
||||
const items = await PremiumService.loadProductsAndSubs();
|
||||
pricingPlans.forEach((plan) => {
|
||||
plan.subscriptions = {};
|
||||
plan.products = {};
|
||||
plan.subscriptionSkuList.forEach((sku) => {
|
||||
if (!plan.subscriptions) plan.subscriptions = {};
|
||||
plan.subscriptions[sku] = items.subs.find((p) => p.productId === sku);
|
||||
try {
|
||||
const items = await PremiumService.loadProductsAndSubs();
|
||||
pricingPlans.forEach((plan) => {
|
||||
plan.subscriptions = {};
|
||||
plan.products = {};
|
||||
plan.subscriptionSkuList.forEach((sku) => {
|
||||
if (!plan.subscriptions) plan.subscriptions = {};
|
||||
plan.subscriptions[sku] = items.subs.find(
|
||||
(p) => p.productId === sku
|
||||
);
|
||||
});
|
||||
plan.productSkuList.forEach((sku) => {
|
||||
if (!plan.products) plan.products = {};
|
||||
plan.products[sku] = items.products.find(
|
||||
(p) => p.productId === sku
|
||||
);
|
||||
});
|
||||
});
|
||||
plan.productSkuList.forEach((sku) => {
|
||||
if (!plan.products) plan.products = {};
|
||||
plan.products[sku] = items.products.find((p) => p.productId === sku);
|
||||
});
|
||||
});
|
||||
setPlans([...pricingPlans]);
|
||||
setUserCanRequestTrial(hasTrialOffer());
|
||||
if (
|
||||
Config.GITHUB_RELEASE === "true" &&
|
||||
!SettingsService.getProperty("serverUrls")
|
||||
) {
|
||||
try {
|
||||
const products = WebPlanCache || (await db.pricing.products());
|
||||
WebPlanCache = products;
|
||||
setWebPricingPlans(products);
|
||||
} catch (e) {
|
||||
/**
|
||||
setPlans([...pricingPlans]);
|
||||
setUserCanRequestTrial(hasTrialOffer());
|
||||
if (
|
||||
Config.GITHUB_RELEASE === "true" &&
|
||||
!SettingsService.getProperty("serverUrls")
|
||||
) {
|
||||
try {
|
||||
const products = WebPlanCache || (await db.pricing.products());
|
||||
WebPlanCache = products;
|
||||
setWebPricingPlans(products);
|
||||
} catch (e) {
|
||||
/**
|
||||
empty */
|
||||
}
|
||||
}
|
||||
}
|
||||
setLoadingPlans(false);
|
||||
setLoadingPlans(false);
|
||||
} catch (e) {}
|
||||
};
|
||||
loadPlans();
|
||||
}, [options?.promoOffer, cancelPromo, hasTrialOffer]);
|
||||
|
||||
@@ -32,6 +32,8 @@ const useTimer = (initialId?: string) => {
|
||||
const [id, setId] = useState(initialId);
|
||||
const [seconds, setSeconds] = useState(getSecondsLeft(id));
|
||||
const interval = useRef<NodeJS.Timeout>(undefined);
|
||||
const secondsRef = useRef(seconds);
|
||||
secondsRef.current = seconds;
|
||||
|
||||
const start = (sec: number, currentId = id) => {
|
||||
if (!currentId) return;
|
||||
@@ -59,7 +61,7 @@ const useTimer = (initialId?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { seconds, setId, start, reset };
|
||||
return { seconds, setId, start, reset, secondsRef: secondsRef };
|
||||
};
|
||||
|
||||
export default useTimer;
|
||||
|
||||
@@ -35,7 +35,6 @@ import { db } from "../../common/database";
|
||||
import { Dialog } from "../../components/dialog";
|
||||
import { Header } from "../../components/header";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Input from "../../components/ui/input";
|
||||
import { ReminderTime } from "../../components/ui/reminder-time";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -59,6 +58,11 @@ import { TimeSince } from "../../components/ui/time-since";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import { eOnLoadNote } from "../../utils/events";
|
||||
import { fluidTabsRef } from "../../utils/global-refs";
|
||||
import FormInput, {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../components/ui/input/form-input";
|
||||
import AppIcon from "../../components/ui/AppIcon";
|
||||
|
||||
const ReminderModes =
|
||||
Platform.OS === "ios"
|
||||
@@ -113,7 +117,12 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
const [repeatFrequency, setRepeatFrequency] = useState(1);
|
||||
const referencedItem = reference ? (reference as Note) : null;
|
||||
const recurringReminderFeature = useIsFeatureAvailable("recurringReminders");
|
||||
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
title: reminder?.title || referencedItem?.title || "",
|
||||
description: reminder?.description || referencedItem?.headline || ""
|
||||
})
|
||||
);
|
||||
const title = useRef<string | undefined>(
|
||||
!reminder ? referencedItem?.title : reminder?.title
|
||||
);
|
||||
@@ -132,6 +141,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
: null,
|
||||
[reminder?.id]
|
||||
);
|
||||
const [dateError, setDateError] = useState<string>();
|
||||
const [selectDayError, setSelectDayError] = useState<string>();
|
||||
|
||||
const showDatePicker = () => {
|
||||
setDatePickerVisibility(true);
|
||||
@@ -143,6 +154,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
|
||||
const handleConfirm = (date: Date) => {
|
||||
timer.current = setTimeout(() => {
|
||||
setDateError(undefined);
|
||||
hideDatePicker();
|
||||
setDate(date);
|
||||
}, 10);
|
||||
@@ -172,26 +184,27 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
|
||||
async function saveReminder() {
|
||||
try {
|
||||
if (!(await Notifications.checkAndRequestPermissions(true)))
|
||||
throw new Error(strings.noNotificationPermission());
|
||||
if (!date && reminderMode !== ReminderModes.Permanent) return;
|
||||
if (!formRef.current.validate()) return;
|
||||
if (date.getTime() < Date.now() && reminderMode === "once") {
|
||||
setDateError(strings.dateError());
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
reminderMode === ReminderModes.Repeat &&
|
||||
recurringMode !== "day" &&
|
||||
recurringMode !== "year" &&
|
||||
selectedDays.length === 0
|
||||
)
|
||||
throw new Error(strings.selectDayError());
|
||||
|
||||
if (!title.current) throw new Error(strings.setTitleError());
|
||||
if (
|
||||
date.getTime() < Date.now() &&
|
||||
reminderMode === "once" &&
|
||||
!props.route.params.reminder
|
||||
) {
|
||||
throw new Error(strings.dateError());
|
||||
setSelectDayError(strings.selectDayError());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!date && reminderMode !== ReminderModes.Permanent) return;
|
||||
|
||||
if (!(await Notifications.checkAndRequestPermissions(true)))
|
||||
throw new Error(strings.noNotificationPermission());
|
||||
|
||||
date.setSeconds(0, 0);
|
||||
|
||||
const reminderId = await db.reminders?.add({
|
||||
@@ -261,7 +274,10 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Input
|
||||
<FormInput
|
||||
name="title"
|
||||
validators={[validators.required(strings.titleIsRequired())]}
|
||||
formRef={formRef}
|
||||
fwdRef={titleRef}
|
||||
defaultValue={reminder?.title || referencedItem?.title}
|
||||
placeholder={strings.remindeMeOf()}
|
||||
@@ -270,12 +286,15 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
wrapperStyle={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
onSubmit={() => {
|
||||
onSubmitEditing={() => {
|
||||
descriptionRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
<FormInput
|
||||
name="description"
|
||||
validators={[]}
|
||||
formRef={formRef}
|
||||
defaultValue={
|
||||
reminder ? reminder?.description : referencedItem?.headline
|
||||
}
|
||||
@@ -461,6 +480,22 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
{selectDayError ? (
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
color: colors.error.icon
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{selectDayError}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -476,16 +511,23 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
<DateTimePickerModal
|
||||
isVisible={isDatePickerVisible}
|
||||
mode="datetime"
|
||||
minimumDate={
|
||||
reminderMode === "once" ? dayjs().toDate() : new Date(0)
|
||||
}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={hideDatePicker}
|
||||
isDarkModeEnabled={isDark}
|
||||
firstDayOfWeek={weekFormat === "Mon" ? 1 : 0}
|
||||
is24Hour={db.settings.getTimeFormat() === "24-hour"}
|
||||
date={date || new Date(Date.now())}
|
||||
themeVariant={isDark ? "dark" : "light"}
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
date={date}
|
||||
minimumDate={
|
||||
reminderMode === "once" ? dayjs().toDate() : new Date(0)
|
||||
}
|
||||
maximumDate={dayjs(date).add(3, "months").toDate()}
|
||||
onDateChange={handleConfirm}
|
||||
theme={isDark ? "dark" : "light"}
|
||||
@@ -519,6 +561,23 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dateError ? (
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
color: colors.error.icon
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{dateError}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
@@ -47,6 +47,14 @@ export async function verifyUser(
|
||||
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) {
|
||||
@@ -95,6 +103,14 @@ export async function verifyUserWithApplock() {
|
||||
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({
|
||||
|
||||
@@ -31,9 +31,10 @@ import dayjs from "dayjs";
|
||||
import React from "react";
|
||||
import { Appearance, Linking, Platform } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import { TextInput } from "react-native-gesture-handler";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import { AppLockPassword } from "../../components/dialogs/applock-password";
|
||||
@@ -42,43 +43,45 @@ import ExportNotesSheet from "../../components/sheets/export-notes";
|
||||
import { Issue } from "../../components/sheets/github/issue";
|
||||
import { Progress } from "../../components/sheets/progress";
|
||||
import { Update } from "../../components/sheets/update";
|
||||
import {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../components/ui/input/form-input";
|
||||
import { VaultStatusType, useVaultStatus } from "../../hooks/use-vault-status";
|
||||
import { BackgroundSync } from "../../services/background-sync";
|
||||
import BackupService from "../../services/backup";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import {
|
||||
ToastManager,
|
||||
VaultRequestType,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
openVault,
|
||||
presentSheet,
|
||||
VaultRequestType
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import Notifications from "../../services/notifications";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import Sync from "../../services/sync";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { refreshAllStores } from "../../stores/create-db-collection-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { EDITOR_LINE_HEIGHT } from "../../utils/constants";
|
||||
import {
|
||||
eAfterSync,
|
||||
eCloseSheet,
|
||||
eOpenRecoveryKeyDialog
|
||||
} from "../../utils/events";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { resetTabStore } from "../editor/tiptap/use-tab-store";
|
||||
import { MFARecoveryCodes, MFASheet } from "./2fa";
|
||||
import { useDragState } from "./editor/state";
|
||||
import { verifyUser, verifyUserWithApplock } from "./functions";
|
||||
import { logoutUser } from "./logout";
|
||||
import { SettingSection } from "./types";
|
||||
import { getTimeLeft } from "./user-section";
|
||||
import { EDITOR_LINE_HEIGHT } from "../../utils/constants";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import { resetTabStore } from "../editor/tiptap/use-tab-store";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { refreshAllStores } from "../../stores/create-db-collection-store";
|
||||
|
||||
export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
@@ -234,18 +237,29 @@ export const settingsGroups: SettingSection[] = [
|
||||
presentDialog({
|
||||
title: strings.redeemGiftCode(),
|
||||
paragraph: strings.redeemGiftCodeDesc(),
|
||||
input: true,
|
||||
inputPlaceholder: strings.code(),
|
||||
positiveText: strings.redeem(),
|
||||
positivePress: async (value) => {
|
||||
db.subscriptions.redeemCode(value).catch((e) => {
|
||||
ToastManager.show({
|
||||
heading: "Error redeeming code",
|
||||
message: (e as Error).message,
|
||||
type: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
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()
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -515,6 +529,14 @@ export const settingsGroups: SettingSection[] = [
|
||||
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 () => {
|
||||
|
||||
@@ -201,6 +201,14 @@ const SettingsUserSection = ({ item }) => {
|
||||
inputPlaceholder: strings.enterFullName(),
|
||||
defaultValue: userProfile?.fullName,
|
||||
positivePress: async (value) => {
|
||||
if (!value || !value.trim()) {
|
||||
ToastManager.error(
|
||||
new Error(strings.nameIsRequired()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
db.settings
|
||||
.setProfile({
|
||||
fullName: value
|
||||
|
||||
@@ -65,6 +65,14 @@ export async function unlockVault({
|
||||
paragraph: paragraph,
|
||||
inputPlaceholder: strings.enterPassword(),
|
||||
positivePress: async (value) => {
|
||||
if (!value || !value.trim()) {
|
||||
ToastManager.error(
|
||||
new Error(strings.passwordNotEntered()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const unlocked = await db.vault.unlock(value);
|
||||
if (!unlocked) {
|
||||
ToastManager.show({
|
||||
|
||||
@@ -47,6 +47,7 @@ import { FlexScrollContainer } from "../../components/scroll-container";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
DropdownSettingComponent,
|
||||
Section,
|
||||
SectionGroup,
|
||||
SectionKeys,
|
||||
Setting,
|
||||
@@ -254,9 +255,15 @@ export const SettingsDialog = DialogManager.register(function SettingsDialog(
|
||||
overflow: "auto"
|
||||
}}
|
||||
>
|
||||
{activeSettings.map((group) => (
|
||||
<SettingsGroupComponent item={group} />
|
||||
))}
|
||||
{activeSettings.length > 0 ? (
|
||||
activeSettings.map((group) => (
|
||||
<SettingsGroupComponent item={group} />
|
||||
))
|
||||
) : (
|
||||
<Text variant="body" sx={{ color: "paragraph-secondary" }}>
|
||||
{strings.noResultsFound()}
|
||||
</Text>
|
||||
)}
|
||||
</FlexScrollContainer>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
@@ -317,12 +324,17 @@ function SettingsSideBar(props: SettingsSideBarProps) {
|
||||
SettingsGroups.filter((g) => g.section === route)
|
||||
);
|
||||
|
||||
const groups: SettingsGroup[] = [];
|
||||
let groups: SettingsGroup[] = [];
|
||||
for (const group of SettingsGroups) {
|
||||
const section = findSection(group.section);
|
||||
if (section?.isHidden?.() || group.isHidden?.()) continue;
|
||||
|
||||
const isTitleMatch =
|
||||
typeof group.header === "string" &&
|
||||
group.header.toLowerCase().includes(query);
|
||||
const isSectionMatch = group.section.includes(query);
|
||||
const isSectionMatch = group.section
|
||||
.toLowerCase()
|
||||
.includes(query);
|
||||
|
||||
if (isTitleMatch || isSectionMatch) {
|
||||
groups.push(group);
|
||||
@@ -347,6 +359,18 @@ function SettingsSideBar(props: SettingsSideBarProps) {
|
||||
if (!settings.length) continue;
|
||||
groups.push({ ...group, settings });
|
||||
}
|
||||
const matchedSections = findSections(query);
|
||||
if (matchedSections.length > 0) {
|
||||
const matchedGroups = SettingsGroups.filter((g) =>
|
||||
matchedSections.some((s) => s.key === g.section)
|
||||
);
|
||||
// remove groups whose sections were matched to avoid duplicate
|
||||
// entries.
|
||||
groups = groups.filter(
|
||||
(g) => !matchedGroups.some((mg) => mg.section === g.section)
|
||||
);
|
||||
groups.push(...matchedGroups);
|
||||
}
|
||||
onNavigate(groups);
|
||||
}}
|
||||
/>
|
||||
@@ -717,3 +741,22 @@ function NumberInput({
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function findSection(key: SectionKeys) {
|
||||
for (const group of sectionGroups) {
|
||||
const section = group.sections.find((s) => s.key === key);
|
||||
if (section) return section;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findSections(query: string) {
|
||||
const sections: Section[] = [];
|
||||
for (const group of sectionGroups) {
|
||||
for (const section of group.sections) {
|
||||
if (section.isHidden?.()) continue;
|
||||
if (section.title.toLowerCase().includes(query)) sections.push(section);
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2026-04-20 11:33+0500\n"
|
||||
"POT-Creation-Date: 2026-05-08 10:43+0500\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
@@ -935,6 +935,10 @@ msgstr "Are you sure you want to clear all logs from {key}?"
|
||||
msgid "Are you sure you want to clear trash?"
|
||||
msgstr "Are you sure you want to clear trash?"
|
||||
|
||||
#: src/strings.ts:2666
|
||||
msgid "Are you sure you want to delete this attachment?"
|
||||
msgstr "Are you sure you want to delete this attachment?"
|
||||
|
||||
#: src/strings.ts:1542
|
||||
msgid "Are you sure you want to logout and clear all data stored on THIS DEVICE?"
|
||||
msgstr "Are you sure you want to logout and clear all data stored on THIS DEVICE?"
|
||||
@@ -988,6 +992,10 @@ msgstr "attachment"
|
||||
msgid "Attachment"
|
||||
msgstr "Attachment"
|
||||
|
||||
#: src/strings.ts:2667
|
||||
msgid "Attachment deleted"
|
||||
msgstr "Attachment deleted"
|
||||
|
||||
#: src/strings.ts:2458
|
||||
msgid "Attachment manager"
|
||||
msgstr "Attachment manager"
|
||||
@@ -1960,10 +1968,15 @@ msgstr "Curate the toolbar that fits your needs and matches your personality."
|
||||
msgid "Current note"
|
||||
msgstr "Current note"
|
||||
|
||||
#: src/strings.ts:1479
|
||||
#: src/strings.ts:1485
|
||||
msgid "Current password"
|
||||
msgstr "Current password"
|
||||
|
||||
#: src/strings.ts:2670
|
||||
msgid "Current password required"
|
||||
msgstr "Current password required"
|
||||
|
||||
#: src/strings.ts:1229
|
||||
msgid "Current path: {path}"
|
||||
msgstr "Current path: {path}"
|
||||
@@ -2142,6 +2155,10 @@ msgstr "Delete"
|
||||
msgid "Delete account"
|
||||
msgstr "Delete account"
|
||||
|
||||
#: src/strings.ts:2664
|
||||
msgid "Delete attachment"
|
||||
msgstr "Delete attachment"
|
||||
|
||||
#: src/strings.ts:1319
|
||||
msgid "Delete collapsed section"
|
||||
msgstr "Delete collapsed section"
|
||||
@@ -3101,6 +3118,10 @@ msgstr "Getting information"
|
||||
msgid "Getting recovery codes"
|
||||
msgstr "Getting recovery codes"
|
||||
|
||||
#: src/strings.ts:2663
|
||||
msgid "Gift code required"
|
||||
msgstr "Gift code required"
|
||||
|
||||
#: src/strings.ts:2172
|
||||
msgid "GNU GENERAL PUBLIC LICENSE Version 3"
|
||||
msgstr "GNU GENERAL PUBLIC LICENSE Version 3"
|
||||
@@ -4018,6 +4039,10 @@ msgstr "Multi-layer encryption to most important notes"
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: src/strings.ts:2669
|
||||
msgid "Name is required."
|
||||
msgstr "Name is required."
|
||||
|
||||
#: src/strings.ts:1688
|
||||
msgid "Native high-performance encryption"
|
||||
msgstr "Native high-performance encryption"
|
||||
@@ -4387,10 +4412,6 @@ msgstr "Okay"
|
||||
msgid "Old - new"
|
||||
msgstr "Old - new"
|
||||
|
||||
#: src/strings.ts:1479
|
||||
msgid "Old password"
|
||||
msgstr "Old password"
|
||||
|
||||
#: src/strings.ts:1835
|
||||
msgid "Older version"
|
||||
msgstr "Older version"
|
||||
@@ -6648,6 +6669,10 @@ msgstr "Title"
|
||||
msgid "Title format"
|
||||
msgstr "Title format"
|
||||
|
||||
#: src/strings.ts:2668
|
||||
msgid "Title is required"
|
||||
msgstr "Title is required"
|
||||
|
||||
#: src/strings.ts:1188
|
||||
msgid "To use app lock, you must enable biometrics such as Fingerprint lock or Face ID on your phone."
|
||||
msgstr "To use app lock, you must enable biometrics such as Fingerprint lock or Face ID on your phone."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2026-04-20 11:33+0500\n"
|
||||
"POT-Creation-Date: 2026-05-08 10:43+0500\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
@@ -935,6 +935,10 @@ msgstr ""
|
||||
msgid "Are you sure you want to clear trash?"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2666
|
||||
msgid "Are you sure you want to delete this attachment?"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1542
|
||||
msgid "Are you sure you want to logout and clear all data stored on THIS DEVICE?"
|
||||
msgstr ""
|
||||
@@ -988,6 +992,10 @@ msgstr ""
|
||||
msgid "Attachment"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2667
|
||||
msgid "Attachment deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2458
|
||||
msgid "Attachment manager"
|
||||
msgstr ""
|
||||
@@ -1949,10 +1957,15 @@ msgstr ""
|
||||
msgid "Current note"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1479
|
||||
#: src/strings.ts:1485
|
||||
msgid "Current password"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2670
|
||||
msgid "Current password required"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1229
|
||||
msgid "Current path: {path}"
|
||||
msgstr ""
|
||||
@@ -2131,6 +2144,10 @@ msgstr ""
|
||||
msgid "Delete account"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2664
|
||||
msgid "Delete attachment"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1319
|
||||
msgid "Delete collapsed section"
|
||||
msgstr ""
|
||||
@@ -3083,6 +3100,10 @@ msgstr ""
|
||||
msgid "Getting recovery codes"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2663
|
||||
msgid "Gift code required"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2172
|
||||
msgid "GNU GENERAL PUBLIC LICENSE Version 3"
|
||||
msgstr ""
|
||||
@@ -3998,6 +4019,10 @@ msgstr ""
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2669
|
||||
msgid "Name is required."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1688
|
||||
msgid "Native high-performance encryption"
|
||||
msgstr ""
|
||||
@@ -4361,10 +4386,6 @@ msgstr ""
|
||||
msgid "Old - new"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1479
|
||||
msgid "Old password"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1835
|
||||
msgid "Older version"
|
||||
msgstr ""
|
||||
@@ -6607,6 +6628,10 @@ msgstr ""
|
||||
msgid "Title format"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2668
|
||||
msgid "Title is required"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1188
|
||||
msgid "To use app lock, you must enable biometrics such as Fingerprint lock or Face ID on your phone."
|
||||
msgstr ""
|
||||
|
||||
@@ -1476,7 +1476,7 @@ $day$: Current day (eg. Monday)`,
|
||||
someNotesPublished: () => t`Some notes are published`,
|
||||
unpublishToDelete: () => t`Unpublish notes to delete them`,
|
||||
filterAttachments: () => t`Filter attachments by filename, type or hash`,
|
||||
oldPassword: () => t`Old password`,
|
||||
oldPassword: () => t`Current password`,
|
||||
newPassword: () => t`New password`,
|
||||
email: () => t`Email`,
|
||||
emailInvalid: () => t`Invalid email`,
|
||||
@@ -2659,5 +2659,13 @@ Use this if changes from other devices are not appearing on this device. This wi
|
||||
t`Value must be between ${min} and ${max}`,
|
||||
passwordRequired: () => t`Password required`,
|
||||
confirmPasswordRequired: () => t`Confirm password required`,
|
||||
enterAValidEmailAddress: () => t`Please enter a valid email address`
|
||||
enterAValidEmailAddress: () => t`Please enter a valid email address`,
|
||||
giftCodeRequired: () => t`Gift code required`,
|
||||
deleteAttachment: () => t`Delete attachment`,
|
||||
deleteAttachmentConfirm: () =>
|
||||
t`Are you sure you want to delete this attachment?`,
|
||||
attachmentDeleted: () => t`Attachment deleted`,
|
||||
titleIsRequired: () => t`Title is required`,
|
||||
nameIsRequired: () => t`Name is required.`,
|
||||
currentPasswordRequired: () => t`Current password required`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user