Compare commits

...

48 Commits

Author SHA1 Message Date
Ammar Ahmed
14d680c092 mobile: show all errors inline with inputs/data-entry items 2026-05-07 18:42:47 +05:00
Ammar Ahmed
28f95b0061 mobile: fix plans keep loading on paywall screen 2026-05-07 13:01:38 +05:00
Ammar Ahmed
419dd555f4 mobile: fix reminder allows past time in edit mode without validation error 2026-05-07 13:01:37 +05:00
Ammar Ahmed
1312ce7d50 Merge pull request #9818 from streetwriters/fix/284
Trim title before checking if title is valid in add reminder screen
2026-05-07 12:58:46 +05:00
Ammar Ahmed
d16cd510b3 mobile: cleanup 2026-05-07 12:58:05 +05:00
Ammar Ahmed
af08ed5174 mobile: show title is required error under the input 2026-05-07 12:58:05 +05:00
Ammar Ahmed
365e1d7029 mobile: trim title before checking if title is valid 2026-05-07 12:57:25 +05:00
Ammar Ahmed
80b75fc28c Merge pull request #9821 from streetwriters/fix/287
Show confirm dialog before deleting attachment in attachment manager
2026-05-07 12:54:10 +05:00
Ammar Ahmed
6de4b4ac2d mobile: show toast message after attachment deleted 2026-05-07 12:53:44 +05:00
Ammar Ahmed
39bfff6b14 mobile: fix base dialog background is always transparent 2026-05-07 12:53:44 +05:00
Ammar Ahmed
e381b62e5c mobile: show confirm dialog before deleting attachment in attachment manager 2026-05-07 12:53:43 +05:00
Ammar Ahmed
ffef5affce Merge pull request #9822 from streetwriters/fix/309
mobile: fix handling invalid gift code input
2026-05-07 12:28:27 +05:00
Ammar Ahmed
780276b30e mobile: fix pro user walk through makes app get stuck 2026-05-07 11:12:49 +05:00
Ammar Ahmed
276239dd2b mobile: add form validation support in dialog inputs 2026-05-07 08:49:21 +05:00
Ammar Ahmed
5ef3bdcad4 mobile: fix handling invalid gift code 2026-05-07 08:03:45 +05:00
Ammar Ahmed
63076d37f5 Merge pull request #9808 from streetwriters/fix/270
Improve error handling when signing up on mobile
2026-05-06 13:21:56 +05:00
Ammar Ahmed
724ad58824 mobile: show errors under the email input 2026-05-06 11:28:58 +05:00
Ammar Ahmed
7fcdb3b55d mobile: skip showing toast message 2026-05-06 08:16:35 +05:00
Ammar Ahmed
47ba40d7cd mobile: fix ts errors in build 2026-05-05 21:00:17 +05:00
Ammar Ahmed
0801e73242 mobile: fix login and signup flows 2026-05-05 19:06:42 +05:00
Ammar Ahmed
c82487983c Merge pull request #9806 from streetwriters/fix/268
Fix No error message shown for missing or incorrect 2FA code on mobile
2026-05-05 14:13:20 +05:00
Ammar Ahmed
2ad39048da mobile: fix form validation in login, signup and forgot password 2026-05-05 13:47:37 +05:00
Ammar Ahmed
3421627f31 mobile: show error if passwords do not match or fields empty 2026-05-05 10:34:44 +05:00
Ammar Ahmed
cf6262e995 mobile: fix showing 2fa error when field is empty and show error below input field 2026-05-05 10:32:27 +05:00
Ammar Ahmed
8a325bf7c0 mobile: cleanup 2026-05-05 10:11:19 +05:00
Ammar Ahmed
883667b411 mobile: show error message on failed two-factor auth 2026-05-05 10:11:19 +05:00
Ammar Ahmed
06b88e74ae Merge pull request #9807 from streetwriters/fix/269
Remove password length check on signup on mobile
2026-05-05 10:10:03 +05:00
Ammar Ahmed
7800a25775 Merge pull request #9803 from streetwriters/mobile/release-3.3.22
mobile: release v3.3.22
2026-05-05 07:46:27 +05:00
Ammar Ahmed
01d0de3909 mobile: remove password length check on signup 2026-05-04 13:42:05 +05:00
Ammar Ahmed
d61f9472c8 mobile: fix search in archived notes returns no results 2026-05-04 13:08:05 +05:00
Ammar Ahmed
2465337148 mobile: fix show results count in search 2026-05-04 13:05:54 +05:00
01zulfi
af4638ac50 Merge pull request #9802 from streetwriters/release/3.3.17
web: bump version to 3.3.17
2026-05-04 12:33:11 +05:00
Ammar Ahmed
5e6be09aab mobile: release v3.3.22 2026-05-04 10:41:36 +05:00
Ammar Ahmed
f626953e0a Merge pull request #9781 from streetwriters/fix/archive-note-time
Fix Archive screen does not show correct date/time on note based on Sorting Type
2026-05-04 10:30:02 +05:00
01zulfi
4d4bce301d web: bump version to 3.3.17
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-04 10:21:03 +05:00
Ammar Ahmed
d45b18afd6 Merge pull request #9783 from streetwriters/feat/compact-view-search
mobile: add compact-view support in search results
2026-05-04 09:51:01 +05:00
Ammar Ahmed
4f212453a6 mobile: fix sort options not working in trash 2026-05-02 11:04:11 +05:00
Ammar Ahmed
2c1b9081bf mobile: fix group type usage
Explicitly pass group type to list and sort sheet.
2026-05-02 10:49:08 +05:00
01zulfi
7c5dc1615c web: remove testimonial with broken link (#9789)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-02 08:48:34 +05:00
Ammar Ahmed
3fb07369bb mobile: fix navigating from Monographs to Search screen 2026-05-01 10:09:50 +05:00
Ammar Ahmed
9b480950da mobile: fix typo in properties key 2026-05-01 09:59:44 +05:00
01zulfi
9f20d243fa web: disable expand sidebar button on small screens (#9769)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-01 09:07:28 +05:00
01zulfi
13e6573342 web: show copied text when monograph link is copied (#9780)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-30 17:31:08 +05:00
Ammar Ahmed
f37869cb5a mobile: add compact-view support in search results 2026-04-30 13:55:19 +05:00
Ammar Ahmed
46d802548b mobile: fix Archive screen does not show correct date/time on note based on sorting 2026-04-30 13:27:52 +05:00
Ammar Ahmed
2bf42c207d Merge pull request #9776 from streetwriters/mobile/release-3.3.21
mobile: v3.3.21 release
2026-04-30 10:05:04 +05:00
Ammar Ahmed
65e42e5d8b mobile: prepare for v3.3.21 release 2026-04-29 16:36:09 +05:00
Ammar Ahmed
c85007f0dd Merge pull request #9705 from streetwriters/fix/screenguard
mobile: fix screenguard on mobile & privacy mode
2026-04-29 16:20:06 +05:00
59 changed files with 1183 additions and 372 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.3.16",
"version": "3.3.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.3.16",
"version": "3.3.17",
"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.16",
"version": "3.3.17",
"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 3100
versionCode 3103
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')

View File

@@ -44,7 +44,6 @@ 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);
@@ -52,7 +51,6 @@ const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
ScreenGuardModule.initSettings();
RNBootSplash.hide({
fade: true
});

View File

@@ -177,26 +177,52 @@ const Actions = ({
{
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"
}

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

View File

@@ -37,8 +37,9 @@ 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 Input from "../ui/input";
import FormInput, { validators } from "../ui/input/form-input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
@@ -63,14 +64,13 @@ export const Login = ({
const {
step,
setStep,
password,
email,
emailInputRef,
passwordInputRef,
loading,
setLoading,
setError,
login
login,
error,
formRef
} = useLogin(async () => {
eSendEvent(eUserLoggedIn, true);
await sleep(500);
@@ -93,6 +93,11 @@ export const Login = ({
});
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
const onContinue = () => {
login();
};
useEffect(() => {
async () => {
setStep(LoginSteps.emailAuth);
@@ -198,27 +203,27 @@ export const Login = ({
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Input
<FormInput
name="email"
formRef={formRef}
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
keyboardType="email-address"
marginBottom={0}
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
defaultValue={email.current}
editable={step === LoginSteps.emailAuth && !loading}
onSubmit={() => {
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
if (step === LoginSteps.emailAuth) {
login();
onContinue();
} else {
passwordInputRef.current?.focus();
}
@@ -227,11 +232,10 @@ export const Login = ({
{step === LoginSteps.passwordAuth && (
<>
<Input
<FormInput
name="password"
formRef={formRef}
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
testID="input.password"
returnKeyLabel={strings.done()}
returnKeyType="done"
@@ -242,9 +246,9 @@ export const Login = ({
placeholder={strings.password()}
marginBottom={0}
editable={!loading}
defaultValue={password.current}
onSubmit={() => {
login();
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
onContinue();
}}
/>
<Button
@@ -255,9 +259,13 @@ export const Login = ({
paddingHorizontal: 0
}}
onPress={() => {
if (loading || !email.current) return;
if (loading) return;
presentSheet({
component: <ForgotPassword userEmail={email.current} />
component: (
<ForgotPassword
userEmail={formRef.current.getValue("email")}
/>
)
});
}}
textStyle={{
@@ -273,8 +281,7 @@ export const Login = ({
<Button
loading={loading}
onPress={() => {
if (loading) return;
login();
onContinue();
}}
style={{
width: "100%"
@@ -328,6 +335,25 @@ 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,29 +43,26 @@ 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, password, email, passwordInputRef, loading, login } = useLogin(
() => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
},
true
);
const { step, passwordInputRef, loading, login, formRef } = useLogin(() => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
}, true);
const logout = async () => {
try {
@@ -104,7 +101,7 @@ export const SessionExpired = () => {
if (!complete) {
const user = await db.user.getUser();
if (!user) return;
email.current = user.email;
formRef.current.setValue("email", user.email);
setVisible(true);
setFocused(false);
return;
@@ -117,14 +114,14 @@ export const SessionExpired = () => {
} catch (e) {
const user = await db.user.getUser();
if (!user) return;
email.current = user.email;
formRef.current.setValue("email", user.email);
setFocused(false);
setVisible(true);
useUserStore.setState({
disableAppLockRequests: true
});
}
}, [email]);
}, [formRef]);
useEffect(() => {
const sub = eSubscribeEvent(eLoginSessionExpired, open);
@@ -155,6 +152,7 @@ export const SessionExpired = () => {
enableSheetKeyboardHandler={true}
visible={true}
>
<Dialog context="two_factor_verify" />
<View
style={{
width: focused ? "100%" : "99.9%",
@@ -192,17 +190,17 @@ export const SessionExpired = () => {
}}
>
{strings.sessionExpiredDesc(
getObfuscatedEmail(email.current as string)
getObfuscatedEmail(formRef.current.getValue("email") as string)
)}
</Paragraph>
</View>
{step === LoginSteps.passwordAuth ? (
<Input
<FormInput
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
formRef={formRef}
name="password"
validators={[validators.required(strings.passwordRequired())]}
returnKeyLabel={strings.done()}
returnKeyType="next"
secureTextEntry
@@ -210,7 +208,7 @@ export const SessionExpired = () => {
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
onSubmit={() => {
onSubmitEditing={() => {
login();
}}
/>

View File

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

View File

@@ -28,6 +28,7 @@ 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,
@@ -39,49 +40,37 @@ export const useLogin = (
onFinishLogin?: () => void,
sessionExpired = false
) => {
const [error, setError] = useState(false);
const [error, setError] = useState<Error>();
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 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 formRef = useRef(
createFormRef({
email: "",
password: ""
})
);
const login = async () => {
if (!validateInfo() || error) return;
try {
if (loading) return;
setError(undefined);
setLoading(true);
switch (step) {
case LoginSteps.emailAuth: {
if (!email.current) {
if (formRef.current.validateField("email")) {
setLoading(false);
return;
}
const mfaInfo = await db.user.authenticateEmail(email.current);
const mfaInfo = await db.user.authenticateEmail(
formRef.current.getValue("email")
);
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,
@@ -103,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);
}
}
},
@@ -119,13 +111,14 @@ export const useLogin = (
break;
}
case LoginSteps.passwordAuth: {
if (!email.current || !password.current) {
if (!formRef.current.validate()) {
setLoading(false);
return;
}
const values = formRef.current.getValues();
await db.user.authenticatePassword(
email.current,
password.current,
values.email,
values.password,
undefined,
sessionExpired
);
@@ -142,12 +135,11 @@ export const useLogin = (
const finishWithError = async (e: Error) => {
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
setLoading(false);
ToastManager.show({
heading: strings.loginFailed(),
message: e.message,
type: "error",
context: "local"
});
if (e.message === "Password is incorrect.") {
formRef.current.setError("password", e.message);
} else {
setError(e);
}
};
const finishLogin = async () => {
@@ -174,13 +166,12 @@ export const useLogin = (
login,
step,
setStep,
email,
password,
passwordInputRef,
emailInputRef,
loading,
setLoading,
error,
setError
setError,
formRef
};
};

View File

@@ -136,7 +136,7 @@ const BaseDialog = ({
? background
: transparent
? "transparent"
: "rgba(0,0,0,0.3)"
: "rgba(0,0,0,0.1)"
}}
>
<KeyboardAvoidingView

View File

@@ -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;

View File

@@ -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,9 +60,34 @@ 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();
setLoading(true);
try {
const isValid = await formRef.current.validate();
if (!isValid) {
setLoading(false);
return;
}
if (dialogInfo.form.onFormSubmit) {
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;
@@ -76,6 +108,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
setChecked(false);
values.current.inputValue = undefined;
formRef.current = undefined;
setVisible(false);
};
@@ -85,6 +118,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 +128,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 +169,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 +216,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 +259,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
}}
testID="input-value"
secureTextEntry={dialogInfo.secureTextEntry}
//defaultValue={dialogInfo.defaultValue}
defaultValue={dialogInfo.defaultValue}
onSubmit={() => {
onPressPositive();
}}
@@ -237,7 +312,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}

View File

@@ -17,7 +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 { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
import {
GroupHeader,
GroupingKey,
GroupOptions,
ItemType
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
@@ -40,7 +45,9 @@ type SectionHeaderProps = {
color?: string;
screen?: RouteName;
groupOptions: GroupOptions;
group: GroupingKey;
onOpenJumpToDialog: () => void;
itemCount?: number;
};
export const SectionHeader = React.memo<
@@ -53,11 +60,13 @@ export const SectionHeader = React.memo<
color,
screen,
groupOptions,
onOpenJumpToDialog
group,
onOpenJumpToDialog,
itemCount
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
dataType as "note" | "notebook" | "searchResult"
);
return (
@@ -104,7 +113,9 @@ export const SectionHeader = React.memo<
color={color || colors.primary.accent}
>
{!item.title || item.title === ""
? strings.pinned().toUpperCase()
? screen === "Search"
? strings.results(itemCount || 0)
: strings.pinned().toUpperCase()
: item.title.toUpperCase()}
</Heading>
</Pressable>
@@ -133,6 +144,7 @@ export const SectionHeader = React.memo<
<Sort
screen={screen}
type={dataType}
group={group}
hideGroupOptions={
screen === "Reminders" || screen === "Search"
}
@@ -150,7 +162,8 @@ export const SectionHeader = React.memo<
hidden={
dataType !== "note" &&
dataType !== "notebook" &&
screen !== "Notes"
screen !== "Notes" &&
screen !== "Search"
}
style={{
width: 25,
@@ -163,9 +176,11 @@ export const SectionHeader = React.memo<
}
onPress={() => {
SettingsService.set({
[dataType !== "notebook"
? "notesListMode"
: "notebooksListMode"]: !isCompactModeEnabled
[dataType === "notebook"
? "notebooksListMode"
: dataType === "searchResult"
? "searchListMode"
: "notesListMode"]: !isCompactModeEnabled
? "compact"
: "normal"
});
@@ -191,6 +206,7 @@ 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,6 +31,7 @@ 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;
};
@@ -38,6 +39,9 @@ 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);
@@ -87,7 +91,7 @@ export const SearchResult = (props: SearchResultProps) => {
flexShrink: 1
}}
>
{props.item.content?.length ? (
{props.item.content?.length && !compactMode ? (
<IconButton
name={!expanded ? "chevron-right" : "chevron-down"}
onPress={() => setExpanded((prev) => !prev)}
@@ -130,6 +134,7 @@ export const SearchResult = (props: SearchResultProps) => {
</View>
{expanded &&
!compactMode &&
props.item.content.map((content, index) => (
<Pressable
key={props.item.id + index}

View File

@@ -53,6 +53,7 @@ type ListProps = {
isRenderedInActionSheet?: boolean;
CustomListComponent?: React.JSX.ElementType;
placeholder?: PlaceholderData;
groupType: GroupingKey;
id?: string;
};
@@ -73,16 +74,7 @@ export default function List(props: ListProps) {
props.dataType === "notebook" ||
notebooksListMode === "compact";
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 groupOptions = useGroupOptions(props.groupType);
const _onRefresh = async () => {
Sync.run("global", false, "full", () => {
@@ -105,7 +97,7 @@ export default function List(props: ListProps) {
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupOptions={groupOptions}
group={groupType as GroupingKey}
group={props.groupType as GroupingKey}
renderedInRoute={props.renderedInRoute}
customAccentColor={props.customAccentColor}
dataType={props.dataType}
@@ -115,7 +107,7 @@ export default function List(props: ListProps) {
},
[
groupOptions,
groupType,
props.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,6 +182,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
item={groupHeader}
index={index}
dataType={item.type}
group={group}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -218,6 +219,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
item={groupHeader}
index={index}
dataType={item.type}
group={group}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -245,6 +247,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
@@ -271,6 +274,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
@@ -297,9 +301,11 @@ 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,6 +121,17 @@ 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,6 +181,7 @@ export default function ReminderNotify({
data={references}
loading={false}
dataType="note"
groupType="notes"
isRenderedInActionSheet={true}
/>
</View>

View File

@@ -45,25 +45,16 @@ import Paragraph from "../../ui/typography/paragraph";
const Sort = ({
type,
screen,
hideGroupOptions
hideGroupOptions,
group: groupType
}: {
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)
);
@@ -157,10 +148,15 @@ const Sort = ({
>
{Object.keys(SORT).map((item) => {
const sortOptionVisibility = {
relevance: screen === "Search",
dueDate: screen === "Reminders",
dateModified: screen === "Tags" || screen === "Reminders",
dateEdited: screen !== "Tags" && screen !== "Reminders"
dateCreated: groupType !== "trash",
relevance: groupType === "search",
dueDate: groupType === "reminders",
dateModified: groupType === "reminders" || groupType === "tags",
dateEdited:
groupType !== "tags" &&
groupType !== "reminders" &&
groupType !== "trash",
dateDeleted: groupType === "trash"
};
// Check if this sort option should be skipped for the current screen
@@ -188,10 +184,7 @@ const Sort = ({
onPress={async () => {
const _groupOptions: GroupOptions = {
...groupOptions,
sortBy:
type === "trash"
? "dateDeleted"
: (item as SortOptions["sortBy"])
sortBy: 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,6 +498,11 @@ const TabBar = (props: SimpleTabBarProps) => {
? "notebook"
: "tag"
}
group={
props.navigationState.index === 1
? "notebooks"
: "tags"
}
hideGroupOptions
/>
)

View File

@@ -0,0 +1,408 @@
/*
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

@@ -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();

View File

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

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 { 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]);

View File

@@ -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;

View File

@@ -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>
)}

View File

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

View File

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

View File

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

View File

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

View File

@@ -90,6 +90,7 @@ export const Reminders = ({
<List
data={reminders}
dataType="reminder"
groupType="reminders"
headerTitle={strings.routes[route.name]()}
renderedInRoute="Reminders"
loading={loading}
@@ -98,9 +99,8 @@ 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,6 +154,7 @@ 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

@@ -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()
});
}
},

View File

@@ -98,6 +98,7 @@ 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 { NativeModules, Platform } from "react-native";
import { Platform } from "react-native";
import { MMKV } from "../common/database/mmkv";
import {
SettingStore,
@@ -29,6 +29,7 @@ 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,6 +58,7 @@ export type Settings = {
appLockMode?: "none" | "background" | "launch";
notebooksListMode?: "normal" | "compact";
notesListMode?: "normal" | "compact";
searchListMode?: "normal" | "compact";
devMode?: boolean;
notifNotes?: boolean;
pitchBlack?: boolean;
@@ -172,6 +173,7 @@ export const defaultSettings: SettingStore["settings"] = {
appLockMode: "none",
notebooksListMode: "normal",
notesListMode: "normal",
searchListMode: "normal",
devMode: false,
notifNotes: false,
pitchBlack: false,

View File

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

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2180
IOS_MARKETING_VERSION = 3.3.20
IOS_CURRENT_PROJECT_VERSION = 2182
IOS_MARKETING_VERSION = 3.3.22
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 = 2180
IOS_MARKETING_VERSION = 3.3.20
IOS_CURRENT_PROJECT_VERSION = 2182
IOS_MARKETING_VERSION = 3.3.22
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 = 2180
IOS_MARKETING_VERSION = 3.3.20
IOS_CURRENT_PROJECT_VERSION = 2182
IOS_MARKETING_VERSION = 3.3.22
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.20",
"version": "3.3.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.3.20",
"version": "3.3.22",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

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

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "3.3.16",
"version": "3.3.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "3.3.16",
"version": "3.3.17",
"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.16",
"version": "3.3.17",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

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

View File

@@ -60,6 +60,7 @@ 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 };
@@ -110,12 +111,15 @@ function PublishView(props: PublishViewProps) {
<Button
variant="secondary"
className="copyPublishLink"
sx={{ flexShrink: 0, m: 0 }}
sx={{ flexShrink: 0, m: 0, color: copied ? "accent" : "initial" }}
onClick={() => {
writeText(`${hosts.MONOGRAPH_HOST}/${monograph?.id}`);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}}
disabled={copied}
>
{strings.copy()}
{copied ? strings.copied() : strings.copy()}
</Button>
</Flex>
) : null}

View File

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

View File

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

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.all
const sortedNoteIds = await this.db.notes.exportable
.fields(["notes.id"])
.items(matches.ids, sortOptions);
const sorted: Matches = { ids: [], values: [] };

View File

@@ -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"
@@ -1770,6 +1778,10 @@ 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"
@@ -2138,6 +2150,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"
@@ -3097,6 +3113,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"
@@ -4540,6 +4560,10 @@ 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"
@@ -4647,6 +4671,7 @@ 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"
@@ -6639,6 +6664,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."
@@ -7662,8 +7691,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 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."
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."
#: src/strings.ts:956
msgid "Your tags"

View File

@@ -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 ""
@@ -1759,6 +1767,10 @@ msgstr ""
msgid "Confirm password"
msgstr ""
#: src/strings.ts:2661
msgid "Confirm password required"
msgstr ""
#: src/strings.ts:1487
msgid "Confirm pin"
msgstr ""
@@ -2127,6 +2139,10 @@ msgstr ""
msgid "Delete account"
msgstr ""
#: src/strings.ts:2664
msgid "Delete attachment"
msgstr ""
#: src/strings.ts:1319
msgid "Delete collapsed section"
msgstr ""
@@ -3079,6 +3095,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 ""
@@ -4514,6 +4534,10 @@ msgstr ""
msgid "Password protection"
msgstr ""
#: src/strings.ts:2660
msgid "Password required"
msgstr ""
#: src/strings.ts:809
msgid "Password updated"
msgstr ""
@@ -4621,6 +4645,7 @@ 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 ""
@@ -6598,6 +6623,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 ""
@@ -7604,7 +7633,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 at {email}. If this email is not reachable or incorrect, 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 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 ""
#: src/strings.ts:956

View File

@@ -2656,5 +2656,14 @@ 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}`
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`,
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`
};