Compare commits

..

1 Commits

Author SHA1 Message Date
01zulfi
26afe29e1c common: don't prefix attachment hash in filenames during export
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-05 16:37:32 +05:00
52 changed files with 323 additions and 858 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.3.17",
"version": "3.3.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.3.17",
"version": "3.3.16",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.17",
"version": "3.3.16",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -140,7 +140,7 @@ android {
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
versionCode 3103
versionCode 3102
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')

View File

@@ -44,6 +44,7 @@ import { useUserStore } from "./stores/use-user-store";
import RNBootSplash from "react-native-bootsplash";
import AppLocked from "./components/app-lock";
import { useSettingStore } from "./stores/use-setting-store";
import ScreenGuardModule from "react-native-screenguard";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
@@ -51,6 +52,7 @@ const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
ScreenGuardModule.initSettings();
RNBootSplash.hide({
fade: true
});

View File

@@ -27,7 +27,7 @@ import { useThemeColors } from "@notesnook/theme";
import DialogHeader from "../dialog/dialog-header";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -36,22 +36,21 @@ import { DefaultAppStyles } from "../../utils/styles";
export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
const { colors } = useThemeColors("sheet");
const formRef = useRef(
createFormRef({
email: userEmail || ""
})
);
const email = useRef<string>(userEmail);
const emailInputRef = useRef<TextInput>(null);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [sent, setSent] = useState(false);
const sendRecoveryEmail = async () => {
if (formRef.current.validateField("email")) {
if (!email.current || error) {
ToastManager.show({
heading: strings.emailRequired(),
type: "error",
context: "local"
});
return;
}
const values = formRef.current.getValues();
setLoading(true);
try {
const lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
@@ -61,7 +60,7 @@ export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
) {
throw new Error(strings.pleaseWaitBeforeSendEmail());
}
await db.user.recoverAccount(values.email.toLowerCase());
await db.user.recoverAccount(email.current.toLowerCase());
SettingsService.set({
lastRecoveryEmailTime: Date.now()
});
@@ -76,7 +75,12 @@ export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
setSent(true);
} catch (e) {
setLoading(false);
formRef.current.setError("email", (e as Error).message);
ToastManager.show({
heading: strings.recoveryEmailFailed(),
message: (e as Error).message,
type: "error",
context: "local"
});
}
};
@@ -122,25 +126,22 @@ export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
<DialogHeader title={strings.accountRecovery()} />
<Seperator />
<FormInput
name="email"
formRef={formRef}
<Input
fwdRef={emailInputRef}
loading={loading}
onChangeText={(value) => {
email.current = value;
}}
defaultValue={email.current}
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.next()}
returnKeyType="next"
autoComplete="email"
keyboardType="email-address"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
sendRecoveryEmail();
}}
onSubmit={() => {}}
/>
<Button

View File

@@ -37,9 +37,8 @@ import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import { Progress } from "../sheets/progress";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import FormInput, { validators } from "../ui/input/form-input";
import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
@@ -64,13 +63,14 @@ export const Login = ({
const {
step,
setStep,
password,
email,
emailInputRef,
passwordInputRef,
loading,
setLoading,
login,
error,
formRef
setError,
login
} = useLogin(async () => {
eSendEvent(eUserLoggedIn, true);
await sleep(500);
@@ -93,11 +93,6 @@ export const Login = ({
});
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
const onContinue = () => {
login();
};
useEffect(() => {
async () => {
setStep(LoginSteps.emailAuth);
@@ -203,27 +198,27 @@ export const Login = ({
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<FormInput
name="email"
formRef={formRef}
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
keyboardType="email-address"
validationType="email"
marginBottom={0}
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
defaultValue={email.current}
editable={step === LoginSteps.emailAuth && !loading}
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
onSubmit={() => {
if (step === LoginSteps.emailAuth) {
onContinue();
login();
} else {
passwordInputRef.current?.focus();
}
@@ -232,10 +227,11 @@ export const Login = ({
{step === LoginSteps.passwordAuth && (
<>
<FormInput
name="password"
formRef={formRef}
<Input
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
testID="input.password"
returnKeyLabel={strings.done()}
returnKeyType="done"
@@ -246,9 +242,9 @@ export const Login = ({
placeholder={strings.password()}
marginBottom={0}
editable={!loading}
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
onContinue();
defaultValue={password.current}
onSubmit={() => {
login();
}}
/>
<Button
@@ -259,13 +255,9 @@ export const Login = ({
paddingHorizontal: 0
}}
onPress={() => {
if (loading) return;
if (loading || !email.current) return;
presentSheet({
component: (
<ForgotPassword
userEmail={formRef.current.getValue("email")}
/>
)
component: <ForgotPassword userEmail={email.current} />
});
}}
textStyle={{
@@ -281,7 +273,8 @@ export const Login = ({
<Button
loading={loading}
onPress={() => {
onContinue();
if (loading) return;
login();
}}
style={{
width: "100%"
@@ -335,25 +328,6 @@ export const Login = ({
</Paragraph>
</TouchableOpacity>
) : null}
{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.message}
</Paragraph>
) : null}
</View>
</View>
</View>

View File

@@ -43,26 +43,29 @@ import SheetProvider from "../sheet-provider";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
import { strings } from "@notesnook/intl";
import { getObfuscatedEmail } from "../../utils/functions";
import { DefaultAppStyles } from "../../utils/styles";
import FormInput, { validators } from "../ui/input/form-input";
export const SessionExpired = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [focused, setFocused] = useState(false);
const { step, passwordInputRef, loading, login, formRef } = useLogin(() => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
}, true);
const { step, password, email, passwordInputRef, loading, login } = useLogin(
() => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
},
true
);
const logout = async () => {
try {
@@ -101,7 +104,7 @@ export const SessionExpired = () => {
if (!complete) {
const user = await db.user.getUser();
if (!user) return;
formRef.current.setValue("email", user.email);
email.current = user.email;
setVisible(true);
setFocused(false);
return;
@@ -114,14 +117,14 @@ export const SessionExpired = () => {
} catch (e) {
const user = await db.user.getUser();
if (!user) return;
formRef.current.setValue("email", user.email);
email.current = user.email;
setFocused(false);
setVisible(true);
useUserStore.setState({
disableAppLockRequests: true
});
}
}, [formRef]);
}, [email]);
useEffect(() => {
const sub = eSubscribeEvent(eLoginSessionExpired, open);
@@ -152,7 +155,6 @@ export const SessionExpired = () => {
enableSheetKeyboardHandler={true}
visible={true}
>
<Dialog context="two_factor_verify" />
<View
style={{
width: focused ? "100%" : "99.9%",
@@ -190,17 +192,17 @@ export const SessionExpired = () => {
}}
>
{strings.sessionExpiredDesc(
getObfuscatedEmail(formRef.current.getValue("email") as string)
getObfuscatedEmail(email.current as string)
)}
</Paragraph>
</View>
{step === LoginSteps.passwordAuth ? (
<FormInput
<Input
fwdRef={passwordInputRef}
formRef={formRef}
name="password"
validators={[validators.required(strings.passwordRequired())]}
onChangeText={(value) => {
password.current = value;
}}
returnKeyLabel={strings.done()}
returnKeyType="next"
secureTextEntry
@@ -208,7 +210,7 @@ export const SessionExpired = () => {
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
onSubmitEditing={() => {
onSubmit={() => {
login();
}}
/>

View File

@@ -30,6 +30,7 @@ import {
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
import Navigation from "../../services/navigation";
import { useUserStore } from "../../stores/use-user-store";
@@ -38,14 +39,13 @@ import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Loading } from "../loading";
import { Button } from "../ui/button";
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AuthHeader } from "./header";
import { SignupContext } from "./signup-context";
import { RouteParams } from "../../stores/use-navigation-store";
import SettingsService from "../../services/settings";
import AppIcon from "../ui/AppIcon";
const SignupSteps = {
signup: 0,
@@ -62,17 +62,13 @@ export const Signup = ({
}) => {
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
const { colors } = useThemeColors();
const formRef = useRef(
createFormRef({
email: "",
password: "",
confirmPassword: ""
})
);
const email = useRef<string>(undefined);
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>(undefined);
const confirmPasswordInputRef = useRef<TextInput>(null);
const [errorMessage, setErrorMessage] = useState<string>();
const confirmPassword = useRef<string>(undefined);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const setLastSynced = useUserStore((state) => state.setLastSynced);
@@ -80,17 +76,29 @@ export const Signup = ({
const isTablet = width > 600;
const route = useRoute<RouteProp<RouteParams, "Auth">>();
const signup = async () => {
setErrorMessage(undefined);
if (!formRef.current.validate()) return;
if (loading) return;
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastManager.show({
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
type: "error",
context: "local"
});
const values = formRef.current.getValues();
return false;
}
return true;
};
const signup = async () => {
if (!validateInfo() || error) return;
if (loading) return;
setLoading(true);
try {
setCurrentStep(SignupSteps.createAccount);
await db.user.signup(values.email.toLowerCase(), values.password);
await db.user.signup(email.current!.toLowerCase(), password.current!);
const user = await db.user.getUser();
setUser(user);
setLastSynced(await db.lastSynced());
@@ -107,14 +115,12 @@ export const Signup = ({
} catch (e) {
setCurrentStep(SignupSteps.signup);
setLoading(false);
if (
(e as Error).message === "Unable to create an account on this email."
) {
formRef.current.setError("email", (e as Error).message);
} else {
setErrorMessage((e as Error).message);
}
ToastManager.show({
heading: strings.signupFailed(),
message: (e as Error).message,
type: "error",
context: "local"
});
return false;
}
};
@@ -209,55 +215,60 @@ export const Signup = ({
alignSelf: "center"
}}
>
<FormInput
name="email"
formRef={formRef}
<Input
fwdRef={emailInputRef}
loading={loading}
onChangeText={(value) => {
email.current = value;
}}
defaultValue={email.current}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
keyboardType="email-address"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
blurOnSubmit={false}
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
onSubmit={() => {
if (!email.current) return;
passwordInputRef.current?.focus();
}}
/>
<FormInput
name="password"
formRef={formRef}
<Input
fwdRef={passwordInputRef}
loading={loading}
onChangeText={(value) => {
password.current = value;
}}
defaultValue={password.current}
testID="input.password"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
blurOnSubmit={false}
validationType="password"
autoCorrect={false}
placeholder={strings.password()}
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
onSubmit={() => {
if (!password.current) return;
confirmPasswordInputRef.current?.focus();
}}
/>
<FormInput
name="confirmPassword"
formRef={formRef}
<Input
fwdRef={confirmPasswordInputRef}
loading={loading}
onChangeText={(value) => {
confirmPassword.current = value;
}}
defaultValue={confirmPassword.current}
testID="input.confirmPassword"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Signup"
returnKeyType="done"
secureTextEntry
@@ -265,22 +276,17 @@ export const Signup = ({
autoCapitalize="none"
autoCorrect={false}
blurOnSubmit={false}
validationType="confirmPassword"
customValidator={() => password.current!}
placeholder={strings.confirmPassword()}
marginBottom={12}
validators={[
validators.required(strings.confirmPasswordRequired()),
validators.matchField(
"password",
strings.passwordNotMatched()
)
]}
onSubmitEditing={() => {
onSubmit={() => {
signup();
}}
/>
<Button
title={!loading ? strings.continue() : null}
title={!loading ? "Continue" : null}
type="accent"
loading={loading}
onPress={() => {
@@ -314,25 +320,6 @@ export const Signup = ({
</Paragraph>
</Paragraph>
</TouchableOpacity>
{errorMessage ? (
<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}
/>{" "}
{errorMessage}
</Paragraph>
) : null}
</View>
<View

View File

@@ -27,15 +27,14 @@ 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;
@@ -53,8 +52,7 @@ const TwoFactorVerification = ({
method: string;
code: string;
},
callback: (result: any) => void,
onerror: (e: Error) => void
callback: (result: any) => void
) => Promise<void>;
mfaInfo: MFAInfo;
onCancel: () => void;
@@ -68,26 +66,15 @@ const TwoFactorVerification = ({
method: mfaInfo?.primaryMethod,
isPrimary: true
});
const { seconds, start, reset, secondsRef } = useTimer(currentMethod.method!);
const { seconds, start, reset } = 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) {
setError(
new Error("Please provide a valid multi-factor authentication code.")
);
if (!code.current || code.current.length < 6 || !currentMethod.method)
return;
}
if (!currentMethod.method) {
return;
}
setLoading(true);
setError(undefined);
inputRef.current?.blur();
await onMfaLogin(
{
@@ -99,9 +86,6 @@ const TwoFactorVerification = ({
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
}
setLoading(false);
},
(e) => {
setError(e);
}
);
setLoading(false);
@@ -147,7 +131,7 @@ const TwoFactorVerification = ({
};
const onSendCode = useCallback(async () => {
if (secondsRef.current || sending) return;
if (seconds || sending) return;
setSending(true);
try {
await db.mfa.sendCode(currentMethod.method as "sms" | "email");
@@ -155,18 +139,15 @@ const TwoFactorVerification = ({
setSending(false);
} catch (e) {
setSending(false);
setError(
new Error(`Error sending 2FA Code. Tap "Send code" to try again `)
);
ToastManager.error(e as Error, "Error sending 2FA Code", "local");
}
}, [currentMethod.method, secondsRef, sending, start]);
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
useEffect(() => {
if (currentMethod.method === "sms" || currentMethod.method === "email") {
onSendCode();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentMethod.method]);
}, [currentMethod.method, onSendCode]);
return (
<ScrollView
@@ -253,7 +234,6 @@ const TwoFactorVerification = ({
fwdRef={inputRef}
textAlign="center"
onChangeText={(value) => {
setError(undefined);
code.current = value;
}}
cursorColor={colors.selected.accent}
@@ -261,7 +241,6 @@ const TwoFactorVerification = ({
selectionColor={colors.selected.accent}
onSubmitEditing={onNext}
height={60}
marginBottom={0}
inputStyle={{
fontSize: AppFontSize.lg,
textAlign: "center",
@@ -275,26 +254,10 @@ 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()}
@@ -375,8 +338,7 @@ TwoFactorVerification.present = (
method: string;
code: string;
},
callback: (result: any) => void,
onerror: (e: Error) => void
callback: (result: any) => void
) => Promise<void>,
data: MFAInfo,
onCancel: () => void,

View File

@@ -28,7 +28,6 @@ import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSimpleDialog } from "../../utils/events";
import TwoFactorVerification from "./two-factor";
import { createFormRef } from "../ui/input/form-input";
export const LoginSteps = {
emailAuth: 1,
@@ -40,37 +39,49 @@ export const useLogin = (
onFinishLogin?: () => void,
sessionExpired = false
) => {
const [error, setError] = useState<Error>();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const [step, setStep] = useState(LoginSteps.emailAuth);
const email = useRef<string>(undefined);
const password = useRef<string>(undefined);
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const formRef = useRef(
createFormRef({
email: "",
password: ""
})
);
const validateInfo = () => {
if (
(!password.current && step === LoginSteps.passwordAuth) ||
(!email.current && step === LoginSteps.emailAuth)
) {
ToastManager.show({
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
type: "error",
context: "local"
});
return false;
}
return true;
};
const login = async () => {
if (!validateInfo() || error) return;
try {
if (loading) return;
setError(undefined);
setLoading(true);
switch (step) {
case LoginSteps.emailAuth: {
if (formRef.current.validateField("email")) {
if (!email.current) {
setLoading(false);
return;
}
const mfaInfo = await db.user.authenticateEmail(
formRef.current.getValue("email")
);
const mfaInfo = await db.user.authenticateEmail(email.current);
if (mfaInfo) {
TwoFactorVerification.present(
async (mfa: any, callback: (success: boolean) => void, onerror: (e: Error) => void) => {
async (mfa: any, callback: (success: boolean) => void) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
@@ -92,9 +103,6 @@ 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);
}
}
},
@@ -111,14 +119,13 @@ export const useLogin = (
break;
}
case LoginSteps.passwordAuth: {
if (!formRef.current.validate()) {
if (!email.current || !password.current) {
setLoading(false);
return;
}
const values = formRef.current.getValues();
await db.user.authenticatePassword(
values.email,
values.password,
email.current,
password.current,
undefined,
sessionExpired
);
@@ -135,11 +142,12 @@ export const useLogin = (
const finishWithError = async (e: Error) => {
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
setLoading(false);
if (e.message === "Password is incorrect.") {
formRef.current.setError("password", e.message);
} else {
setError(e);
}
ToastManager.show({
heading: strings.loginFailed(),
message: e.message,
type: "error",
context: "local"
});
};
const finishLogin = async () => {
@@ -166,12 +174,13 @@ export const useLogin = (
login,
step,
setStep,
email,
password,
passwordInputRef,
emailInputRef,
loading,
setLoading,
error,
setError,
formRef
setError
};
};

View File

@@ -17,12 +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 {
GroupHeader,
GroupingKey,
GroupOptions,
ItemType
} from "@notesnook/core";
import { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
@@ -45,9 +40,7 @@ type SectionHeaderProps = {
color?: string;
screen?: RouteName;
groupOptions: GroupOptions;
group: GroupingKey;
onOpenJumpToDialog: () => void;
itemCount?: number;
};
export const SectionHeader = React.memo<
@@ -60,13 +53,11 @@ export const SectionHeader = React.memo<
color,
screen,
groupOptions,
group,
onOpenJumpToDialog,
itemCount
onOpenJumpToDialog
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook" | "searchResult"
dataType as "note" | "notebook"
);
return (
@@ -113,9 +104,7 @@ export const SectionHeader = React.memo<
color={color || colors.primary.accent}
>
{!item.title || item.title === ""
? screen === "Search"
? strings.results(itemCount || 0)
: strings.pinned().toUpperCase()
? strings.pinned().toUpperCase()
: item.title.toUpperCase()}
</Heading>
</Pressable>
@@ -144,7 +133,6 @@ export const SectionHeader = React.memo<
<Sort
screen={screen}
type={dataType}
group={group}
hideGroupOptions={
screen === "Reminders" || screen === "Search"
}
@@ -162,8 +150,7 @@ export const SectionHeader = React.memo<
hidden={
dataType !== "note" &&
dataType !== "notebook" &&
screen !== "Notes" &&
screen !== "Search"
screen !== "Notes"
}
style={{
width: 25,
@@ -176,11 +163,9 @@ export const SectionHeader = React.memo<
}
onPress={() => {
SettingsService.set({
[dataType === "notebook"
? "notebooksListMode"
: dataType === "searchResult"
? "searchListMode"
: "notesListMode"]: !isCompactModeEnabled
[dataType !== "notebook"
? "notesListMode"
: "notebooksListMode"]: !isCompactModeEnabled
? "compact"
: "normal"
});
@@ -206,7 +191,6 @@ export const SectionHeader = React.memo<
},
(prev, next) => {
if (prev.item.title !== next.item.title) return false;
if (prev.itemCount !== next.itemCount) return false;
if (prev.groupOptions?.groupBy !== next.groupOptions.groupBy) return false;
if (prev.groupOptions?.sortDirection !== next.groupOptions.sortDirection)
return false;

View File

@@ -31,7 +31,6 @@ import { eSendEvent } from "../../../services/event-manager";
import { eOnLoadNote } from "../../../utils/events";
import { IconButton } from "../../ui/icon-button";
import { fluidTabsRef } from "../../../utils/global-refs";
import { useSettingStore } from "../../../stores/use-setting-store";
type SearchResultProps = {
item: HighlightedResult;
};
@@ -39,9 +38,6 @@ type SearchResultProps = {
export const SearchResult = (props: SearchResultProps) => {
const [expanded, setExpanded] = React.useState(true);
const { colors } = useThemeColors();
const compactMode = useSettingStore(
(state) => state.settings.searchListMode === "compact"
);
const openNote = async (index?: number) => {
const note = await db.notes.note(props.item.id);
@@ -91,7 +87,7 @@ export const SearchResult = (props: SearchResultProps) => {
flexShrink: 1
}}
>
{props.item.content?.length && !compactMode ? (
{props.item.content?.length ? (
<IconButton
name={!expanded ? "chevron-right" : "chevron-down"}
onPress={() => setExpanded((prev) => !prev)}
@@ -134,7 +130,6 @@ export const SearchResult = (props: SearchResultProps) => {
</View>
{expanded &&
!compactMode &&
props.item.content.map((content, index) => (
<Pressable
key={props.item.id + index}

View File

@@ -53,7 +53,6 @@ type ListProps = {
isRenderedInActionSheet?: boolean;
CustomListComponent?: React.JSX.ElementType;
placeholder?: PlaceholderData;
groupType: GroupingKey;
id?: string;
};
@@ -74,7 +73,16 @@ export default function List(props: ListProps) {
props.dataType === "notebook" ||
notebooksListMode === "compact";
const groupOptions = useGroupOptions(props.groupType);
const groupType =
props.renderedInRoute === "Notes"
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: props.renderedInRoute === "Trash" || props.dataType === "trash"
? "trash"
: `${props.dataType}s`;
const groupOptions = useGroupOptions(groupType);
const _onRefresh = async () => {
Sync.run("global", false, "full", () => {
@@ -97,7 +105,7 @@ export default function List(props: ListProps) {
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupOptions={groupOptions}
group={props.groupType as GroupingKey}
group={groupType as GroupingKey}
renderedInRoute={props.renderedInRoute}
customAccentColor={props.customAccentColor}
dataType={props.dataType}
@@ -107,7 +115,7 @@ export default function List(props: ListProps) {
},
[
groupOptions,
props.groupType,
groupType,
props.customAccentColor,
props.data,
props.dataType,

View File

@@ -53,7 +53,7 @@ import TagItem from "../list-items/tag";
import { SearchResult } from "../list-items/search-result";
type ListItemWrapperProps<TItem = Item> = {
group: GroupingKey;
group?: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
isSheet: boolean;
index: number;
@@ -182,7 +182,6 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
item={groupHeader}
index={index}
dataType={item.type}
group={group}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -219,7 +218,6 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
item={groupHeader}
index={index}
dataType={item.type}
group={group}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -247,7 +245,6 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
@@ -274,7 +271,6 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
@@ -301,11 +297,9 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
itemCount={items?.placeholders.length}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,

View File

@@ -121,17 +121,6 @@ export const RelationsList = ({
<List
data={items}
loading={false}
groupType={
referenceType === "note"
? "notes"
: referenceType === "tag"
? "tags"
: referenceType === "notebook"
? "notebooks"
: referenceType === "reminder"
? "reminders"
: "notes"
}
dataType={referenceType as any}
isRenderedInActionSheet={true}
/>

View File

@@ -181,7 +181,6 @@ export default function ReminderNotify({
data={references}
loading={false}
dataType="note"
groupType="notes"
isRenderedInActionSheet={true}
/>
</View>

View File

@@ -45,16 +45,25 @@ import Paragraph from "../../ui/typography/paragraph";
const Sort = ({
type,
screen,
hideGroupOptions,
group: groupType
hideGroupOptions
}: {
type: ItemType;
screen?: RouteName;
group: GroupingKey;
hideGroupOptions?: boolean;
}) => {
const { colors } = useThemeColors();
const groupType =
screen === "Archive"
? "archive"
: screen === "Search"
? "search"
: screen === "Notes"
? "home"
: screen === "Trash" || type === "trash"
? "trash"
: ((type + "s") as GroupingKey);
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions(groupType)
);
@@ -148,15 +157,10 @@ const Sort = ({
>
{Object.keys(SORT).map((item) => {
const sortOptionVisibility = {
dateCreated: groupType !== "trash",
relevance: groupType === "search",
dueDate: groupType === "reminders",
dateModified: groupType === "reminders" || groupType === "tags",
dateEdited:
groupType !== "tags" &&
groupType !== "reminders" &&
groupType !== "trash",
dateDeleted: groupType === "trash"
relevance: screen === "Search",
dueDate: screen === "Reminders",
dateModified: screen === "Tags" || screen === "Reminders",
dateEdited: screen !== "Tags" && screen !== "Reminders"
};
// Check if this sort option should be skipped for the current screen
@@ -184,7 +188,10 @@ const Sort = ({
onPress={async () => {
const _groupOptions: GroupOptions = {
...groupOptions,
sortBy: item as SortOptions["sortBy"]
sortBy:
type === "trash"
? "dateDeleted"
: (item as SortOptions["sortBy"])
};
await updateGroupOptions(_groupOptions);
}}

View File

@@ -433,7 +433,7 @@ const TabBar = (props: SimpleTabBarProps) => {
name="plus"
testID="sidebar-add-button"
size={AppFontSize.lg - 2}
top={10}
top={10}
color={colors.primary.icon}
onPress={async () => {
if (props.navigationState.index === 1) {
@@ -498,11 +498,6 @@ const TabBar = (props: SimpleTabBarProps) => {
? "notebook"
: "tag"
}
group={
props.navigationState.index === 1
? "notebooks"
: "tags"
}
hideGroupOptions
/>
)

View File

@@ -1,408 +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, { RefObject, useEffect, useMemo, useState } from "react";
import {
ColorValue,
TextInput,
TextInputProps,
TouchableOpacity,
View,
ViewStyle
} from "react-native";
import isEmail from "validator/lib/isEmail";
import isURL from "validator/lib/isURL";
import { useThemeColors } from "@notesnook/theme";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { IconButton } from "../icon-button";
import Paragraph from "../typography/paragraph";
import AppIcon from "../AppIcon";
export type FormValues = Record<string, string>;
export type FormErrors = Partial<Record<string, string>>;
export type FieldValidator = (
value: string,
values: FormValues
) => string | undefined;
export type ValidationSchema = Partial<Record<string, FieldValidator[]>>;
export interface FormRef {
values: FormValues;
errors: FormErrors;
setValue: (name: string, value: string) => void;
getValue: (name: string) => string;
getValues: () => FormValues;
setError: (name: string, error?: string) => void;
getError: (name: string) => string | undefined;
clearErrors: () => void;
registerField: (name: string, validators?: FieldValidator[]) => void;
unregisterField: (name: string) => void;
validateField: (name: string) => string | undefined;
validate: () => boolean;
subscribe: (listener: () => void) => () => void;
}
export function createFormRef(initialValues: FormValues = {}): FormRef {
const listeners = new Set<() => void>();
const values: FormValues = { ...initialValues };
const errors: FormErrors = {};
const schema: ValidationSchema = {};
const notify = () => {
listeners.forEach((listener) => listener());
};
return {
values,
errors,
setValue(name, value) {
values[name] = value;
if (errors[name]) {
delete errors[name];
notify();
}
},
getValue(name) {
return values[name] ?? "";
},
getValues() {
return { ...values };
},
setError(name, error) {
if (!error) {
delete errors[name];
} else {
errors[name] = error;
}
notify();
},
getError(name) {
return errors[name];
},
clearErrors() {
Object.keys(errors).forEach((key) => delete errors[key]);
notify();
},
registerField(name, validators = []) {
schema[name] = validators;
if (values[name] === undefined) values[name] = "";
},
unregisterField(name) {
delete schema[name];
delete errors[name];
notify();
},
validateField(name) {
const fieldValidators = schema[name] || [];
const value = values[name] ?? "";
for (const validator of fieldValidators) {
const error = validator(value, values);
if (error) {
errors[name] = error;
notify();
return error;
}
}
delete errors[name];
notify();
return undefined;
},
validate() {
const nextErrors = validateForm(values, schema);
Object.keys(errors).forEach((key) => delete errors[key]);
Object.keys(nextErrors).forEach((key) => {
errors[key] = nextErrors[key];
});
notify();
return !hasFormErrors(nextErrors);
},
subscribe(listener) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
};
}
export function validateForm(
values: FormValues,
schema: ValidationSchema
): FormErrors {
const errors: FormErrors = {};
Object.keys(schema).forEach((field) => {
const fieldValidators = schema[field] || [];
const value = values[field] ?? "";
for (const validator of fieldValidators) {
const error = validator(value, values);
if (error) {
errors[field] = error;
break;
}
}
});
return errors;
}
export function hasFormErrors(errors: FormErrors) {
return Object.keys(errors).length > 0;
}
export const validators = {
required:
(message = "This field is required") =>
(value: string) =>
value?.trim() ? undefined : message,
email:
(message = "Please enter a valid email") =>
(value: string) =>
!value?.trim() || isEmail(value.trim()) ? undefined : message,
minLength: (length: number, message?: string) => (value: string) =>
!value || value.length >= length
? undefined
: message || `Must be at least ${length} characters`,
url:
(message = "Please enter a valid URL") =>
(value: string) =>
!value?.trim() || isURL(value.trim(), { allow_underscores: true })
? undefined
: message,
matchField:
(fieldName: string, message = "Values do not match") =>
(value: string, values: FormValues) =>
value === values[fieldName] ? undefined : message
};
interface FormInputProps extends TextInputProps {
name: string;
formRef: RefObject<FormRef>;
validators?: FieldValidator[];
fwdRef?: RefObject<TextInput | null>;
loading?: boolean;
error?: string;
customColor?: ColorValue;
marginBottom?: number;
marginRight?: number;
button?: {
icon: string;
color: ColorValue;
onPress: () => void;
testID?: string;
size?: number;
};
buttons?: React.ReactNode;
buttonLeft?: React.ReactNode;
height?: number;
fontSize?: number;
inputStyle?: TextInputProps["style"];
containerStyle?: ViewStyle;
wrapperStyle?: ViewStyle;
}
export function FormInput({
name,
formRef,
validators: fieldValidators = [],
fwdRef,
loading,
error,
secureTextEntry,
customColor,
marginBottom = 10,
marginRight,
button,
buttonLeft,
buttons,
height = 45,
fontSize = AppFontSize.sm,
inputStyle = {},
containerStyle = {},
wrapperStyle = {},
onFocus,
onBlur,
onPress,
onChangeText,
...restProps
}: FormInputProps) {
const { colors, isDark } = useThemeColors();
const [focused, setFocused] = useState(false);
const [secureEntry, setSecureEntry] = useState(true);
const [, setVersion] = useState(0);
useEffect(() => {
const form = formRef.current;
form.registerField(name, fieldValidators);
const unsubscribe = form.subscribe(() => {
setVersion((v) => v + 1);
});
return () => {
unsubscribe();
form.unregisterField(name);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formRef, name]);
const value = formRef.current.getValue(name);
const fieldError = error || formRef.current.getError(name);
const borderColor = useMemo(() => {
if (fieldError) return colors.error.accent;
if (focused) return customColor || colors.selected.border;
return colors.primary.border;
}, [colors, customColor, fieldError, focused]);
const style: ViewStyle = {
borderWidth: 1,
borderRadius: defaultBorderRadius,
borderColor,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
...containerStyle
};
const textStyle: TextInputProps["style"] = {
paddingHorizontal: 0,
fontSize,
color:
onPress && loading ? colors.primary.accent : colors.primary.paragraph,
flexGrow: 1,
flexShrink: 1,
paddingBottom: DefaultAppStyles.GAP_VERTICAL,
paddingTop: DefaultAppStyles.GAP_VERTICAL,
fontFamily: "Inter-Regular",
...(inputStyle as ViewStyle)
};
const handleChangeText = (nextValue: string) => {
formRef.current.setValue(name, nextValue);
onChangeText?.(nextValue);
};
return (
<View
importantForAccessibility="yes"
style={{
marginBottom,
marginRight,
...wrapperStyle
}}
>
<TouchableOpacity
disabled={!loading}
onPress={onPress}
activeOpacity={1}
style={style}
>
{buttonLeft}
<TextInput
{...restProps}
defaultValue={value}
ref={fwdRef}
editable={!loading && restProps.editable !== false}
onChangeText={handleChangeText}
onFocus={(e) => {
setFocused(true);
onFocus?.(e);
}}
onBlur={(e) => {
setFocused(false);
onBlur?.(e);
}}
keyboardAppearance={isDark ? "dark" : "light"}
style={textStyle}
secureTextEntry={secureTextEntry && secureEntry}
placeholderTextColor={colors.primary.placeholder}
/>
<View
style={{
flexDirection: "row",
justifyContent: "center",
height: 35 > height ? height : 35,
alignItems: "center"
}}
>
{secureTextEntry && (
<IconButton
name="eye"
size={20}
top={10}
bottom={10}
onPress={() => {
fwdRef?.current?.blur();
setSecureEntry(!secureEntry);
}}
style={{
width: 25,
marginLeft: 5
}}
color={
secureEntry ? colors.secondary.icon : colors.primary.accent
}
/>
)}
{buttons}
{button && (
<IconButton
testID={button.testID}
name={button.icon}
size={button.size || AppFontSize.xl}
top={10}
bottom={10}
onPress={button.onPress}
color={button.color}
style={{
marginRight: -8
}}
/>
)}
</View>
</TouchableOpacity>
{fieldError ? (
<Paragraph
size={AppFontSize.xs}
style={{ marginTop: 5, color: colors.error.icon }}
>
<AppIcon
color={colors.error.accent}
name="alert-circle-outline"
size={AppFontSize.sm - 1}
/>{" "}
{fieldError}
</Paragraph>
) : null}
</View>
);
}
export default FormInput;

View File

@@ -121,8 +121,8 @@ const Input = ({
const color = error
? colors.error.border
: focus
? customColor || colors.selected.border
: colors.primary.border;
? customColor || colors.selected.border
: colors.primary.border;
const validate = async (value: string) => {
if (!validationType) return;

View File

@@ -20,27 +20,14 @@ import { ItemType } from "@notesnook/core";
import { useSettingStore } from "../stores/use-setting-store";
export function useIsCompactModeEnabled(dataType: ItemType) {
const [notebooksListMode, notesListMode, searchListMode] = useSettingStore(
(state) => [
state.settings.notebooksListMode,
state.settings.notesListMode,
state.settings.searchListMode
]
);
const [notebooksListMode, notesListMode] = useSettingStore((state) => [
state.settings.notebooksListMode,
state.settings.notesListMode
]);
if (
dataType !== "note" &&
dataType !== "notebook" &&
dataType !== "searchResult"
)
return false;
if (dataType !== "note" && dataType !== "notebook") return false;
const listMode =
dataType === "notebook"
? notebooksListMode
: dataType === "searchResult"
? searchListMode
: notesListMode;
const listMode = dataType === "notebook" ? notebooksListMode : notesListMode;
return listMode === "compact";
}

View File

@@ -32,8 +32,6 @@ 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;
@@ -61,7 +59,7 @@ const useTimer = (initialId?: string) => {
}
};
return { seconds, setId, start, reset, secondsRef: secondsRef };
return { seconds, setId, start, reset };
};
export default useTimer;

View File

@@ -70,7 +70,6 @@ export const Archive = ({ navigation, route }: NavigationProps<"Archive">) => {
onRefresh={() => {
refresh();
}}
groupType="archive"
renderedInRoute="Archive"
loading={loading}
placeholder={{

View File

@@ -70,7 +70,6 @@ export const Favorites = ({
<List
data={favorites}
dataType="note"
groupType="favorites"
onRefresh={() => {
refresh();
}}

View File

@@ -70,7 +70,6 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
<List
data={notes}
dataType="note"
groupType="home"
renderedInRoute={route.name}
loading={loading || !isFocused}
headerTitle={strings.routes[route.name]()}

View File

@@ -185,7 +185,6 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
<List
data={notes}
dataType="note"
groupType="notes"
onRefresh={() => {
onRequestUpdate();
}}

View File

@@ -193,14 +193,11 @@ const NotesPage = ({
hasSearch={true}
id={route.name === "Monographs" ? "Monographs" : params?.current?.id}
onSearch={() => {
if (route.name !== "Monographs" && !item) return;
if (!item) return;
const selector =
route.name === "Monographs"
? db.monographs.all
: item && db.relations.from(item, "note").selector;
if (!selector) return;
: db.relations.from(item, "note").selector;
Navigation.push("Search", {
placeholder: strings.searchInRoute(title || route.name),
@@ -217,7 +214,6 @@ const NotesPage = ({
<List
data={notes}
dataType="note"
groupType="notes"
onRefresh={onRequestUpdate}
loading={false}
renderedInRoute={route.name}

View File

@@ -90,7 +90,6 @@ export const Reminders = ({
<List
data={reminders}
dataType="reminder"
groupType="reminders"
headerTitle={strings.routes[route.name]()}
renderedInRoute="Reminders"
loading={loading}
@@ -99,8 +98,9 @@ export const Reminders = ({
paragraph: strings.remindersEmpty(),
button: strings.setReminder(),
action: async () => {
const reminderFeature =
await isFeatureAvailable("activeReminders");
const reminderFeature = await isFeatureAvailable(
"activeReminders"
);
if (!reminderFeature.isAllowed) {
ToastManager.show({
type: "info",

View File

@@ -154,7 +154,6 @@ export const Search = ({ route, navigation }: NavigationProps<"Search">) => {
data={results}
dataType={route.params?.type}
renderedInRoute={route.name}
groupType="search"
loading={loading}
placeholder={{
title: route.name,

View File

@@ -322,8 +322,8 @@ export const MFASetup = ({
method?.id === "email"
? user?.email
: method?.id === "app"
? authenticatorDetails?.sharedKey || ""
: undefined
? authenticatorDetails?.sharedKey || ""
: undefined
}
multiline={method.id === "app"}
onChangeText={(value) => {
@@ -352,20 +352,16 @@ export const MFASetup = ({
<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()
}`
? strings.copy()
: `${
seconds
? strings.resendCode(seconds as number)
: strings.sendCode()
}`
}
/>
)

View File

@@ -98,7 +98,6 @@ export const Trash = ({ navigation, route }: NavigationProps<"Trash">) => {
<List
data={trash}
dataType="trash"
groupType="trash"
renderedInRoute="Trash"
loading={!isFocused}
placeholder={PLACEHOLDER_DATA(db.settings.getTrashCleanupInterval())}

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import { NativeModules, Platform } from "react-native";
import { MMKV } from "../common/database/mmkv";
import {
SettingStore,
@@ -29,7 +29,6 @@ import { scale, updateSize } from "../utils/size";
import { DatabaseLogger } from "../common/database";
import { useUserStore } from "../stores/use-user-store";
import ScreenGuardModule from "react-native-screenguard";
ScreenGuardModule.initSettings();
function reset() {
const settings = get();

View File

@@ -58,7 +58,6 @@ export type Settings = {
appLockMode?: "none" | "background" | "launch";
notebooksListMode?: "normal" | "compact";
notesListMode?: "normal" | "compact";
searchListMode?: "normal" | "compact";
devMode?: boolean;
notifNotes?: boolean;
pitchBlack?: boolean;
@@ -173,7 +172,6 @@ export const defaultSettings: SettingStore["settings"] = {
appLockMode: "none",
notebooksListMode: "normal",
notesListMode: "normal",
searchListMode: "normal",
devMode: false,
notifNotes: false,
pitchBlack: false,

View File

@@ -48,8 +48,7 @@ export const SORT = {
dateCreated: "Date created",
title: "Title",
dueDate: "Due date",
relevance: "Relevance",
dateDeleted: "Date deleted"
relevance: "Relevance"
};
export const itemSkus = [

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2182
IOS_MARKETING_VERSION = 3.3.22
IOS_CURRENT_PROJECT_VERSION = 2181
IOS_MARKETING_VERSION = 3.3.21
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2182
IOS_MARKETING_VERSION = 3.3.22
IOS_CURRENT_PROJECT_VERSION = 2181
IOS_MARKETING_VERSION = 3.3.21
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Staging iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2182
IOS_MARKETING_VERSION = 3.3.22
IOS_CURRENT_PROJECT_VERSION = 2181
IOS_MARKETING_VERSION = 3.3.21
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.3.22",
"version": "3.3.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.3.22",
"version": "3.3.20",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.3.22",
"version": "3.3.21",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "3.3.17",
"version": "3.3.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "3.3.17",
"version": "3.3.16",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "3.3.17",
"version": "3.3.16",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -215,10 +215,7 @@ function DesktopAppContents() {
zIndex: 3
}}
>
<NavigationMenu
onExpand={() => navPane.current?.reset(0)}
canExpand={!isTablet}
/>
<NavigationMenu onExpand={() => navPane.current?.reset(0)} />
</Pane>
)}
{isFocusMode ? null : (

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useMemo } from "react";
import { Box, Button, Flex, Image, Link, Text } from "@theme-ui/components";
import { getRandom, usePromise } from "@notesnook/common";
import Grberk from "../../assets/testimonials/grberk.jpeg";
import Holenstein from "../../assets/testimonials/holenstein.jpg";
import Jason from "../../assets/testimonials/jason.jpg";
import Cameron from "../../assets/testimonials/cameron.jpg";
@@ -28,6 +29,13 @@ import { SettingsDialog } from "../../dialogs/settings";
import { strings } from "@notesnook/intl";
const testimonials = [
{
username: "grberk",
image: Grberk,
name: "Glenn Berkshier",
link: "https://twitter.com/grberk/status/1438955961490751489",
text: "Are you looking for an alternative to @evernote, or just looking for a more secure note taking platform? Take a look at @notesnook and see if it will fit your needs."
},
{
username: "HolensteinDan",
image: Holenstein,

View File

@@ -228,13 +228,7 @@ const tabs: NavigationTabItem[] = [
}
] as const;
function NavigationMenu({
onExpand,
canExpand
}: {
onExpand?: () => void;
canExpand: boolean;
}) {
function NavigationMenu({ onExpand }: { onExpand?: () => void }) {
const isFocusMode = useAppStore((store) => store.isFocusMode);
const navigationTab = useAppStore((store) => store.navigationTab);
const setNavigationTab = useAppStore((store) => store.setNavigationTab);
@@ -358,7 +352,6 @@ function NavigationMenu({
sx={{ p: 1, bg: "transparent" }}
onClick={onExpand}
title={strings.expandSidebar()}
disabled={!canExpand}
>
<ExpandSidebar size={13} color="icon" />
</Button>

View File

@@ -60,7 +60,6 @@ function PublishView(props: PublishViewProps) {
const publishNote = useStore((store) => store.publish);
const unpublishNote = useStore((store) => store.unpublish);
const [monograph, setMonograph] = useState(props.monograph);
const [copied, setCopied] = useState(false);
const monographAnalytics = useIsFeatureAvailable("monographAnalytics");
const analytics = usePromise(async () => {
if (!monographAnalytics?.isAllowed || !monograph) return { totalViews: 0 };
@@ -111,15 +110,12 @@ function PublishView(props: PublishViewProps) {
<Button
variant="secondary"
className="copyPublishLink"
sx={{ flexShrink: 0, m: 0, color: copied ? "accent" : "initial" }}
sx={{ flexShrink: 0, m: 0 }}
onClick={() => {
writeText(`${hosts.MONOGRAPH_HOST}/${monograph?.id}`);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}}
disabled={copied}
>
{copied ? strings.copied() : strings.copy()}
{strings.copy()}
</Button>
</Flex>
) : null}

View File

@@ -1,3 +0,0 @@
- Bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -102,7 +102,10 @@ export async function* exportNotes(
// case where the user has a notebook named attachments
const attachmentsRoot = pathTree.add("attachments", "underscore");
const pendingAttachments: Map<string, Attachment> = new Map();
const resolvedAttachments: Map<
string,
{ path: string; attachment: Attachment }
> = new Map();
for (const [id] of notePathMap) {
const note = await database.notes.note(id);
@@ -120,7 +123,8 @@ export async function* exportNotes(
unlockVault: options.unlockVault,
format,
attachmentsRoot,
pendingAttachments,
resolvedAttachments,
attachmentTree: pathTree,
resolveInternalLink: (link) => {
const internalLink = parseInternalLink(link);
if (!internalLink) return link;
@@ -152,7 +156,7 @@ export async function* exportNotes(
}
}
for (const [path, attachment] of pendingAttachments) {
for (const { path, attachment } of resolvedAttachments.values()) {
yield <ExportableAttachment>{
type: "attachment",
path,
@@ -178,13 +182,18 @@ export async function* exportNote(
});
const ext = FORMAT_TO_EXT[options.format];
const path = [filename, ext].join(".");
const pendingAttachments: Map<string, Attachment> = new Map();
const resolvedAttachments: Map<
string,
{ path: string; attachment: Attachment }
> = new Map();
const attachmentTree = new PathTree();
try {
const content = await exportContent(note, {
format,
attachmentsRoot,
pendingAttachments,
resolvedAttachments,
attachmentTree,
unlockVault: options.unlockVault
});
if (!content) return false;
@@ -197,7 +206,7 @@ export async function* exportNote(
ctime: new Date(note.dateCreated)
};
for (const [path, attachment] of pendingAttachments) {
for (const { path, attachment } of resolvedAttachments.values()) {
yield <ExportableAttachment>{
type: "attachment",
path,
@@ -225,7 +234,8 @@ export async function exportContent(
// TODO: remove these
attachmentsRoot?: string;
pendingAttachments?: Map<string, Attachment>;
resolvedAttachments?: Map<string, { path: string; attachment: Attachment }>;
attachmentTree?: PathTree;
resolveInternalLink?: ResolveInternalLink;
}
) {
@@ -234,7 +244,8 @@ export async function exportContent(
unlockVault,
resolveInternalLink,
attachmentsRoot,
pendingAttachments,
resolvedAttachments,
attachmentTree,
disableTemplate
} = options;
const rawContent = await database.content.findByNoteId(note.id);
@@ -274,7 +285,7 @@ export async function exportContent(
if (
attachmentsRoot &&
pendingAttachments &&
resolvedAttachments &&
format !== "txt" &&
format !== "pdf"
) {
@@ -286,15 +297,32 @@ export async function exportContent(
const sources: Record<string, string> = {};
for (const attachment of attachments) {
const filename = [attachment.hash, attachment.filename].join("-");
const attachmentPath = join(attachmentsRoot, filename);
const existing = resolvedAttachments.get(attachment.hash);
if (existing) {
sources[attachment.hash] = resolveAttachment(
elements,
attachment,
existing.path,
format
);
continue;
}
const baseFilename = attachment.filename || attachment.hash;
const rawAttachmentPath = join(attachmentsRoot, baseFilename);
const attachmentPath =
attachmentTree?.add(rawAttachmentPath) || rawAttachmentPath;
sources[attachment.hash] = resolveAttachment(
elements,
attachment,
attachmentPath,
format
);
pendingAttachments.set(attachmentPath, attachment);
resolvedAttachments.set(attachment.hash, {
path: attachmentPath,
attachment
});
}
return sources;
});

View File

@@ -273,7 +273,7 @@ export default class Lookup {
);
matches.ids = matches.values.map((c) => c.id);
} else {
const sortedNoteIds = await this.db.notes.exportable
const sortedNoteIds = await this.db.notes.all
.fields(["notes.id"])
.items(matches.ids, sortOptions);
const sorted: Matches = { ids: [], values: [] };

View File

@@ -1770,10 +1770,6 @@ msgstr "Confirm new password"
msgid "Confirm password"
msgstr "Confirm password"
#: src/strings.ts:2661
msgid "Confirm password required"
msgstr "Confirm password required"
#: src/strings.ts:1487
msgid "Confirm pin"
msgstr "Confirm pin"
@@ -4544,10 +4540,6 @@ msgstr "Password not entered"
msgid "Password protection"
msgstr "Password protection"
#: src/strings.ts:2660
msgid "Password required"
msgstr "Password required"
#: src/strings.ts:809
msgid "Password updated"
msgstr "Password updated"
@@ -4655,7 +4647,6 @@ msgid "Please enable automatic backups to avoid losing important data."
msgstr "Please enable automatic backups to avoid losing important data."
#: src/strings.ts:1512
#: src/strings.ts:2662
msgid "Please enter a valid email address"
msgstr "Please enter a valid email address"
@@ -7671,8 +7662,8 @@ msgid "Your support request has been forwarded"
msgstr "Your support request has been forwarded"
#: src/strings.ts:2062
msgid "Your support request has been forwarded to our support team. We will get back to you via email as soon as possible. If you don't receive an email from us within 24-48 hours, please send us an email directly at support@notesnook.com."
msgstr "Your support request has been forwarded to our support team. We will get back to you via email as soon as possible. If you don't receive an email from us within 24-48 hours, please send us an email directly at support@notesnook.com."
msgid "Your support request has been forwarded to our support team. We will get back to you via email at {email}. If this email is not reachable or incorrect, please send us an email directly at support@notesnook.com."
msgstr "Your support request has been forwarded to our support team. We will get back to you via email at {email}. If this email is not reachable or incorrect, please send us an email directly at support@notesnook.com."
#: src/strings.ts:956
msgid "Your tags"

View File

@@ -1759,10 +1759,6 @@ msgstr ""
msgid "Confirm password"
msgstr ""
#: src/strings.ts:2661
msgid "Confirm password required"
msgstr ""
#: src/strings.ts:1487
msgid "Confirm pin"
msgstr ""
@@ -4518,10 +4514,6 @@ msgstr ""
msgid "Password protection"
msgstr ""
#: src/strings.ts:2660
msgid "Password required"
msgstr ""
#: src/strings.ts:809
msgid "Password updated"
msgstr ""
@@ -4629,7 +4621,6 @@ msgid "Please enable automatic backups to avoid losing important data."
msgstr ""
#: src/strings.ts:1512
#: src/strings.ts:2662
msgid "Please enter a valid email address"
msgstr ""
@@ -7613,7 +7604,7 @@ msgid "Your support request has been forwarded"
msgstr ""
#: src/strings.ts:2062
msgid "Your support request has been forwarded to our support team. We will get back to you via email as soon as possible. If you don't receive an email from us within 24-48 hours, please send us an email directly at support@notesnook.com."
msgid "Your support request has been forwarded to our support team. We will get back to you via email at {email}. If this email is not reachable or incorrect, please send us an email directly at support@notesnook.com."
msgstr ""
#: src/strings.ts:956

View File

@@ -2656,8 +2656,5 @@ Use this if changes from other devices are not appearing on this device. This wi
t`Invalid recovery key. Make sure to input your account recovery key, not a 2FA recovery code.`,
featureNotAvailable: () => t`This feature is not available on this plan.`,
valueMustBeBetween: (min: number, max: number) =>
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`
t`Value must be between ${min} and ${max}`
};