Compare commits

..

1 Commits

Author SHA1 Message Date
Ammar Ahmed
a0ce43a552 mobile: fix android version code 2026-04-22 08:52:26 +05:00
106 changed files with 2661 additions and 4390 deletions

View File

@@ -101,6 +101,7 @@ jobs:
- name: Build dmg
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.mac_certs }}
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
@@ -152,6 +153,8 @@ jobs:
run: npm run tx @notesnook/desktop:release
- name: Build AppImage
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p never
working-directory: ./apps/desktop
@@ -162,49 +165,6 @@ jobs:
name: linux-x64-build
path: apps/desktop/output/*.AppImage
build-linux-arm64:
name: Build for Linux arm64
needs: build
runs-on: ubuntu-22.04-arm
outputs:
linux-arm64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build AppImage
run: yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
working-directory: ./apps/desktop
- name: Upload AppImage artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: linux-arm64-build
path: apps/desktop/output/*.AppImage
build-windows:
name: Build for Windows
needs: build
@@ -256,8 +216,7 @@ jobs:
post-pr-comment:
name: Post PR comment with preview URLs
needs:
[build, build-macos, build-linux-x64, build-linux-arm64, build-windows]
needs: [build, build-macos, build-linux-x64, build-windows]
runs-on: ubuntu-latest
steps:
- name: Post or update PR comment
@@ -265,7 +224,6 @@ jobs:
env:
macos_artifact_url: ${{ needs.build-macos.outputs.macos-artifact-url }}
linux_x64_artifact_url: ${{ needs.build-linux-x64.outputs.linux-x64-artifact-url }}
linux_arm64_artifact_url: ${{ needs.build-linux-arm64.outputs.linux-arm64-artifact-url }}
windows_artifact_url: ${{ needs.build-windows.outputs.windows-artifact-url }}
with:
script: |
@@ -274,7 +232,6 @@ jobs:
const previewUrl = [
{ platform: 'macOS', url: process.env.macos_artifact_url },
{ platform: 'Linux x64', url: process.env.linux_x64_artifact_url },
{ platform: 'Linux arm64', url: process.env.linux_arm64_artifact_url },
{ platform: 'Windows x64', url: process.env.windows_artifact_url },
].filter(u => u.url).map(u => `- [${u.platform}](${u.url})`).join('\n');
const body = `${marker}\n**Desktop Previews**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;

View File

@@ -198,9 +198,6 @@ module.exports = {
}
}
},
toolsets: {
appimage: "1.0.2"
},
snap: {
autoStart: false,
confinement: "strict",

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.18",
"version": "3.3.14",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
@@ -48,7 +48,7 @@
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.3",
"electron": "^37.0.0",
"electron-builder": "^26.8.1",
"electron-builder": "^26.0.12",
"esbuild": "0.21.5",
"node-abi": "^4.5.0",
"node-gyp-build": "^4.8.4",

View File

@@ -178,9 +178,7 @@ export const osIntegrationRouter = t.router({
})
});
notification.show();
if (input.urgency === "critical" && process.platform !== "linux") {
// due to an Electron bug in versions below 40.x, shell.beep() causes a segfault on Linux.
// TODO: Remove when migrating to Electron 40.x or newer.
if (input.urgency === "critical") {
shell.beep();
}

View File

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

View File

@@ -66,9 +66,6 @@ const App = (props: { configureMode: "note-preview" }) => {
useEffect(() => {
SettingsService.onFirstLaunch();
changeSystemBarColors();
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
setTimeout(async () => {
await Notifications.get();
if (SettingsService.get().notifNotes) {

View File

@@ -22,7 +22,7 @@ import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { RefObject, useEffect, useState } from "react";
import { TextInput, View } from "react-native";
import { View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { ScrollView } from "react-native-gesture-handler";
import { db } from "../../common/database";
@@ -59,7 +59,6 @@ import Paragraph from "../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import Navigation from "../../services/navigation";
import { createFormRef, validators } from "../ui/input/form-input";
const Actions = ({
attachment,
@@ -154,100 +153,50 @@ const Actions = ({
{
name: strings.rename(),
onPress: () => {
close?.();
setTimeout(() => {
presentDialog({
title: strings.renameFile(),
form: {
formRef: createFormRef({
name: attachment.filename
}),
items: [
{
name: "name",
defaultValue: attachment.filename,
placeholder: strings.enterTitle(),
ref: React.createRef<TextInput | null>(),
validators: [validators.required(strings.nameIsRequired())]
}
],
onFormSubmit: async (form) => {
try {
const value = form.getValue("name");
await db.attachments.add({
hash: attachment.hash,
filename: value
});
setFilename(value);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
ToastManager.show({
message: `Attachment renamed to ${value}`,
type: "success"
});
return true;
} catch (e) {
form.setError("name", (e as Error).message);
return false;
}
}
},
positiveText: strings.rename()
});
}, 500);
presentDialog({
input: true,
title: strings.renameFile(),
defaultValue: attachment.filename,
positivePress: async (value) => {
if (value && value.length > 0) {
await db.attachments.add({
hash: attachment.hash,
filename: value
});
setFilename(value);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
}
return true;
},
positiveText: strings.rename()
});
},
icon: "form-textbox"
},
{
name: strings.delete(),
onPress: async () => {
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()
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
});
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;
} else {
editorController.current.commands.setLoading(true, tab.id);
}
}
});
});
}, 500);
close?.();
},
icon: "delete-outline"
}

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
@@ -27,44 +26,42 @@ import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useUserStore } from "../../stores/use-user-store";
import { eOpenRecoveryKeyDialog } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
import Input from "../ui/input";
import { Notice } from "../ui/notice";
import Paragraph from "../ui/typography/paragraph";
import { TextInput } from "react-native-gesture-handler";
export const ChangePassword = () => {
const { colors } = useThemeColors();
const formRef = useRef(
createFormRef({
oldPassword: "",
password: ""
})
);
const oldPasswordInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>(undefined);
const oldPasswordInputRef = useRef<TextInput>(null);
const oldPassword = useRef<string>(undefined);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const user = useUserStore((state) => state.user);
const changePassword = async () => {
setError(undefined);
formRef.current.clearErrors();
if (!user?.isEmailConfirmed) {
setError(strings.emailNotConfirmedDesc());
ToastManager.show({
heading: strings.emailNotConfirmed(),
message: strings.emailNotConfirmedDesc(),
type: "error",
context: "local"
});
return;
}
if (!formRef.current.validate()) {
if (error || !oldPassword.current || !password.current) {
ToastManager.show({
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
type: "error",
context: "local"
});
return;
}
const values = formRef.current.getValues();
setLoading(true);
try {
const result = await BackupService.run(
@@ -77,8 +74,8 @@ export const ChangePassword = () => {
}
const passwordChanged = await db.user.changePassword(
values.oldPassword,
values.password
oldPassword.current,
password.current
);
if (!passwordChanged) {
@@ -94,15 +91,15 @@ export const ChangePassword = () => {
Navigation.goBack();
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
const message = (e as Error).message;
setLoading(false);
if (/old password/i.test(message)) {
formRef.current.setError("oldPassword", message);
} else {
setError(message);
}
ToastManager.show({
heading: strings.passwordChangeFailed(),
message: (e as Error).message,
type: "error",
context: "local"
});
}
setLoading(false);
};
return (
@@ -113,61 +110,36 @@ export const ChangePassword = () => {
}}
>
<Dialog context="change-password-dialog" />
<FormInput
name="oldPassword"
formRef={formRef}
<Input
fwdRef={oldPasswordInputRef}
loading={loading}
validators={[validators.required(strings.currentPasswordRequired())]}
onChangeText={(value) => {
oldPassword.current = value;
}}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.currentPassword()}
onSubmitEditing={() => {
passwordInputRef.current?.focus();
}}
placeholder={strings.oldPassword()}
/>
<FormInput
name="password"
formRef={formRef}
<Input
fwdRef={passwordInputRef}
loading={loading}
validators={[validators.required(strings.passwordRequired())]}
onChangeText={(value) => {
password.current = value;
}}
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
validationType="password"
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.newPassword()}
onSubmitEditing={() => {
changePassword();
}}
/>
{error ? (
<Paragraph
numberOfLines={4}
onPress={() => {}}
color={colors.error.accent}
style={{
textAlign: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
<AppIcon
color={colors.error.accent}
name="alert-circle-outline"
size={AppFontSize.sm - 1}
/>{" "}
{error}
</Paragraph>
) : null}
<Notice text={strings.changePasswordNotice()} type="alert" />
<View style={{ height: 10 }} />

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

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

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ActivityIndicator, StyleSheet, View } from "react-native";
import { StyleSheet, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { getColorLinearShade } from "../../utils/colors";
@@ -79,11 +79,6 @@ const DialogButtons = ({
/>
<Paragraph color={colors.primary.accent}>{" " + doneText}</Paragraph>
</View>
) : loading ? (
<ActivityIndicator
size={AppFontSize.lg}
color={colors.primary.accent}
/>
) : (
<View />
)}
@@ -110,6 +105,7 @@ const DialogButtons = ({
style={{
marginLeft: 10
}}
loading={loading}
bold
type={positiveType || "transparent"}
title={positiveTitle}

View File

@@ -17,12 +17,10 @@ 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, TextInput } from "react-native";
import { KeyboardTypeOptions } 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;
@@ -45,18 +43,6 @@ 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,13 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, {
useCallback,
useEffect,
useRef,
useState,
RefObject
} from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { TextInput, View, ViewStyle } from "react-native";
import { DDS } from "../../services/device-detection";
import {
@@ -40,7 +34,6 @@ 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";
@@ -60,38 +53,15 @@ 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 () => {
// Handle form submission if form is available
if (dialogInfo?.form && formRef.current) {
inputRef.current?.blur();
try {
const isValid = await formRef.current.validate();
if (!isValid) {
return;
}
if (dialogInfo.form.onFormSubmit) {
setLoading(true);
const result = await dialogInfo.form.onFormSubmit(formRef.current);
if (result === false) {
setLoading(false);
return;
}
}
} catch (e) {
/** Empty */
}
setLoading(false);
} else if (dialogInfo?.positivePress) {
// Handle old input-based submission
if (dialogInfo?.positivePress) {
inputRef.current?.blur();
setLoading(true);
let result = false;
try {
result = await dialogInfo.positivePress(
values.current.inputValue,
values.current.inputValue || dialogInfo.defaultValue,
checked
);
} catch (e) {
@@ -106,7 +76,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
setChecked(false);
values.current.inputValue = undefined;
formRef.current = undefined;
setVisible(false);
};
@@ -116,7 +85,6 @@ 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);
},
@@ -126,7 +94,6 @@ 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?.();
@@ -167,30 +134,19 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
? false
: dialogInfo.statusBarTranslucent
}
bounce={!dialogInfo.input && !dialogInfo.form}
bounce={!dialogInfo.input}
closeOnTouch={!dialogInfo.disableBackdropClosing}
background={dialogInfo.background}
transparent={
dialogInfo.transparent === undefined ? false : dialogInfo.transparent
dialogInfo.transparent === undefined ? true : dialogInfo.transparent
}
onShow={async () => {
if (dialogInfo.input && !dialogInfo.form) {
if (dialogInfo.input) {
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}
@@ -214,36 +170,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
/>
<Seperator half />
{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 ? (
{dialogInfo.input ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
@@ -257,7 +184,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
}}
testID="input-value"
secureTextEntry={dialogInfo.secureTextEntry}
defaultValue={dialogInfo.defaultValue}
//defaultValue={dialogInfo.defaultValue}
onSubmit={() => {
onPressPositive();
}}
@@ -310,10 +237,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
<DialogButtons
onPressNegative={onNegativePress}
onPressPositive={
(dialogInfo.positivePress || dialogInfo.form?.onFormSubmit) &&
onPressPositive
}
onPressPositive={dialogInfo.positivePress && onPressPositive}
loading={loading}
positiveTitle={dialogInfo.positiveText}
negativeTitle={dialogInfo.negativeText}

View File

@@ -201,14 +201,12 @@ export const AppLockPassword = () => {
accountPass
? strings.enterAccountPassword()
: mode === "change"
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin"
? strings.pin()
: strings.password()
}`
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin" ? strings.pin() : strings.password()
}`
}
/>
@@ -366,9 +364,7 @@ export const AppLockPassword = () => {
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
SettingsService.setPrivacyScreen(SettingsService.get());
ToastManager.show({
message: strings.applockDisabled(),
type: "success"
@@ -387,8 +383,8 @@ export const AppLockPassword = () => {
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
? strings.change()
: strings.save()
}
negativeTitle={strings.cancel()}
positiveType="transparent"

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

@@ -76,9 +76,7 @@ const MergeConflicts = () => {
const applyChanges = async () => {
const contentToSave = selectedContent;
if (!contentToSave) return;
const note = await db.notes.note(
(content.current as UnencryptedContentItem).noteId
);
const note = await db.notes.note(contentToSave.noteId);
if (!note) return;
await db.notes.add({
id: note.id,

View File

@@ -17,18 +17,18 @@ 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 { Debug, IssueReportResponse } from "@notesnook/core";
import { Debug } from "@notesnook/core";
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { Linking, Platform, Text, TextInput, View } from "react-native";
import { getVersion } from "react-native-device-info";
import { useStoredRef } from "../../../hooks/use-stored-ref";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import { ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
import { useUserStore } from "../../../stores/use-user-store";
import { openLinkInBrowser } from "../../../utils/functions";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size/index";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
@@ -37,26 +37,17 @@ import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import Config from "react-native-config";
import { eCloseSheet } from "../../../utils/events";
export const Issue = ({
defaultTitle,
defaultBody,
issueTitle
}: {
defaultTitle?: string;
defaultBody?: string;
issueTitle?: string;
}) => {
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
const { colors } = useThemeColors();
const body = useStoredRef("issueBody", "");
const body = useStoredRef("issueBody");
const title = useStoredRef("issueTitle", defaultTitle);
const [done, setDone] = useState(false);
const user = useUserStore((state) => state.user);
const [loading, setLoading] = useState(false);
const bodyRef = useRef<TextInput>(null);
const bodyRef = useRef();
const initialLayout = useRef(false);
const issueReportResponse = useRef<IssueReportResponse>(undefined);
const issueUrl = useRef();
const onPress = async () => {
if (loading) return;
@@ -66,7 +57,7 @@ export const Issue = ({
try {
setLoading(true);
issueReportResponse.current = await Debug.report({
issueUrl.current = await Debug.report({
title: title.current,
body:
body.current +
@@ -81,7 +72,7 @@ Logged in: ${user ? "yes" : "no"}
Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
userId: user?.id
});
if (!issueReportResponse.current) {
if (!issueUrl.current) {
setLoading(false);
ToastManager.show({
heading: "Failed to report issue on github",
@@ -97,46 +88,12 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
} catch (e) {
setLoading(false);
ToastManager.show({
heading: (e as Error).message,
heading: e.message,
type: "error"
});
}
};
function getResponseInfo(response?: IssueReportResponse) {
if (!response || "error" in response) return;
switch (response.type) {
case "email": {
return {
title: strings.yourSupportRequestHasBeenForwarded(),
message: strings.supportEmailMessage()
};
}
case "discussion": {
const url = response.url;
return {
title: strings.thankYouForFeedback(),
positiveButtonText: strings.copyLink(),
message: strings.featureRequestMessage(url),
url: url
};
}
case "issue": {
const url = response.url;
return {
title: strings.thankYouForReporting(),
positiveButtonText: strings.copyLink(),
message: strings.bugReportMessage(url),
url: url
};
}
}
}
const responseInfo = getResponseInfo(issueReportResponse.current);
console.log(responseInfo, issueReportResponse.current);
return (
<View
style={{
@@ -148,28 +105,38 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
<>
<View
style={{
height: 250,
justifyContent: "center",
alignItems: "center",
gap: 10
}}
>
<Heading>{responseInfo?.title}</Heading>
<Heading>{strings.issueCreatedHeading()}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
selectable={true}
>
{responseInfo?.message}
{strings.issueCreatedDesc[0]()}
<Paragraph
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
onPress={() => {
Linking.openURL(issueUrl.current);
}}
>
{issueUrl.current}
</Paragraph>
. {strings.issueCreatedDesc[1]()}
</Paragraph>
<Button
title={responseInfo?.positiveButtonText || "Done"}
title={strings.openIssue()}
onPress={() => {
if (responseInfo?.url) {
Linking.openURL(responseInfo?.url);
}
eSendEvent(eCloseSheet);
Linking.openURL(issueUrl.current);
}}
type="accent"
width="100%"
@@ -281,7 +248,10 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
}}
onPress={async () => {
try {
await openLinkInBrowser("https://discord.gg/zQBK97EE22");
await openLinkInBrowser(
"https://discord.gg/zQBK97EE22",
colors
);
} catch (e) {
console.error(e);
}

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,411 +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,
TextStyle,
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;
errorStyle?: TextStyle;
}
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,
errorStyle,
...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 }, errorStyle]}
>
<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

@@ -368,14 +368,7 @@ export const useActions = ({
inputPlaceholder: strings.name(),
defaultValue: item.title,
positivePress: async (value) => {
if (!value || value.trim().length === 0) {
ToastManager.error(
new Error(strings.nameIsRequired()),
undefined,
"local"
);
return;
}
if (!value || value.trim().length === 0) return;
await db.colors.add({
id: item.id,
title: value

View File

@@ -95,7 +95,6 @@ import { BETA } from "../utils/constants";
import {
eAfterSync,
eCloseSheet,
eCloseSimpleDialog,
eEditorReset,
eLoginSessionExpired,
eOnLoadNote,
@@ -287,10 +286,7 @@ const onUserSubscriptionStatusChanged = async (
subscription: subscription
}
});
eSendEvent(eCloseSimpleDialog);
setTimeout(() => {
Walkthrough.present("prouser", false, true);
}, 500);
Walkthrough.present("prouser", false, true);
}
await PremiumService.setPremiumStatus();
useMessageStore.getState().setAnnouncement();

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

@@ -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, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import { useAsync } from "react-async-hook";
import { Platform } from "react-native";
import Config from "react-native-config";
@@ -181,8 +181,6 @@ 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) => {
@@ -192,11 +190,11 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
);
return (
plansRef.current.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
plansRef.current.find((p) => p.id === planId)?.products?.[skuId]
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
plans.find((p) => p.id === planId)?.products?.[skuId]
);
},
[webPricingPlans]
[plans, webPricingPlans]
);
const getProductAndroid = (planId: string, skuId: string) => {
@@ -255,41 +253,35 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
useEffect(() => {
const loadPlans = async () => {
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
);
});
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);
});
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) {
/**
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) {
/**
empty */
}
}
setLoadingPlans(false);
} catch (e) {}
}
setLoadingPlans(false);
};
loadPlans();
}, [options?.promoOffer, cancelPromo, hasTrialOffer]);

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

@@ -35,6 +35,7 @@ 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";
@@ -58,11 +59,6 @@ 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"
@@ -117,12 +113,7 @@ 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
);
@@ -141,8 +132,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
: null,
[reminder?.id]
);
const [dateError, setDateError] = useState<string>();
const [selectDayError, setSelectDayError] = useState<string>();
const showDatePicker = () => {
setDatePickerVisibility(true);
@@ -154,7 +143,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const handleConfirm = (date: Date) => {
timer.current = setTimeout(() => {
setDateError(undefined);
hideDatePicker();
setDate(date);
}, 10);
@@ -184,27 +172,26 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
async function saveReminder() {
try {
if (!formRef.current.validate()) return;
if (date.getTime() < Date.now() && reminderMode === "once") {
setDateError(strings.dateError());
return;
}
if (!(await Notifications.checkAndRequestPermissions(true)))
throw new Error(strings.noNotificationPermission());
if (!date && reminderMode !== ReminderModes.Permanent) 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
) {
setSelectDayError(strings.selectDayError());
return;
throw new Error(strings.dateError());
}
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({
@@ -274,10 +261,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
>
<FormInput
name="title"
validators={[validators.required(strings.titleIsRequired())]}
formRef={formRef}
<Input
fwdRef={titleRef}
defaultValue={reminder?.title || referencedItem?.title}
placeholder={strings.remindeMeOf()}
@@ -286,15 +270,12 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
wrapperStyle={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
onSubmitEditing={() => {
onSubmit={() => {
descriptionRef.current?.focus();
}}
/>
<FormInput
name="description"
validators={[]}
formRef={formRef}
<Input
defaultValue={
reminder ? reminder?.description : referencedItem?.headline
}
@@ -480,22 +461,6 @@ 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}
@@ -511,23 +476,16 @@ 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"}
@@ -561,23 +519,6 @@ 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,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

@@ -21,7 +21,6 @@ import { sanitizeFilename, useIsFeatureAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors, VariantsWithStaticColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import isMobilePhone from "validator/lib/isMobilePhone";
import React, {
Dispatch,
SetStateAction,
@@ -29,13 +28,7 @@ import React, {
useRef,
useState
} from "react";
import {
ActivityIndicator,
Linking,
Platform,
TextInput,
View
} from "react-native";
import { ActivityIndicator, Linking, Platform, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { FlatList } from "react-native-gesture-handler";
import * as ScopedStorage from "react-native-scoped-storage";
@@ -44,10 +37,7 @@ import filesystem from "../../common/filesystem";
import DialogHeader from "../../components/dialog/dialog-header";
import { Button } from "../../components/ui/button";
import { IconButton } from "../../components/ui/icon-button";
import FormInput, {
createFormRef,
validators
} from "../../components/ui/input/form-input";
import Input from "../../components/ui/input";
import { Pressable } from "../../components/ui/pressable";
import Seperator from "../../components/ui/seperator";
import { SvgView } from "../../components/ui/svg";
@@ -193,60 +183,27 @@ export const MFASetup = ({
}: MFAStepProps) => {
const { colors } = useThemeColors();
const user = useUserStore((state) => state.user);
const methodId = method?.id;
const formRef = useRef(
createFormRef({
target: "",
code: ""
})
);
const targetInputRef = useRef<TextInput>(null);
const codeInputRef = useRef<TextInput>(null);
const [authenticatorDetails, setAuthenticatorDetails] = useState({
sharedKey: null,
authenticatorUri: null
});
const code = useRef<string>(undefined);
const phoneNumber = useRef<string>(undefined);
const { seconds, setId, start } = useTimer(method?.id);
const [loading, setLoading] = useState(method?.id === "app" ? true : false);
const [enabling, setEnabling] = useState(false);
const [sending, setSending] = useState(false);
const [generalError, setGeneralError] = useState<string>();
const [error, setError] = useState(false);
useEffect(() => {
if (methodId === "app") {
setLoading(true);
db.mfa
?.setup("app")
.then((data) => {
setAuthenticatorDetails(data);
setLoading(false);
})
.catch((error: Error) => {
setLoading(false);
setGeneralError(error.message);
});
return;
if (method?.id === "app") {
db.mfa?.setup("app").then((data) => {
setAuthenticatorDetails(data);
setLoading(false);
});
}
setLoading(false);
}, [methodId]);
useEffect(() => {
if (!methodId) return;
formRef.current.clearErrors();
formRef.current.setValue(
"target",
methodId === "email"
? user?.email || ""
: methodId === "app"
? authenticatorDetails.sharedKey || ""
: formRef.current.getValue("target")
);
formRef.current.setValue("code", "");
setGeneralError(undefined);
}, [authenticatorDetails.sharedKey, methodId, user?.email]);
}, [method?.id]);
const codeHelpText = {
app: "After putting the above code in authenticator app, the app will display a code that you can enter below.",
@@ -255,43 +212,15 @@ export const MFASetup = ({
"You will receive a 2FA code on your email address which you can enter below"
};
const targetValidators =
method?.id === "sms"
? [
validators.required(strings.phoneNumberNotEntered()),
(value: string) =>
isMobilePhone(value, "any", {
strictMode: true
})
? undefined
: strings.enterValidPhone()
]
: method?.id === "email"
? [
validators.required(strings.emailRequired()),
validators.email(strings.enterValidEmail())
]
: [];
const codeValidators = [
validators.required(strings.enterSixDigitCode()),
(value: string) =>
/^\d{6}$/.test(value.trim()) ? undefined : strings.enterSixDigitCode()
];
const onNext = async () => {
if (formRef.current.validateField("code")) return;
if (!code.current || code.current.length !== 6) return;
try {
if (!method) return;
const code = formRef.current.getValue("code").trim();
setGeneralError(undefined);
setEnabling(true);
if (recovery) {
await db.mfa.enableFallback(method.id, code);
await db.mfa.enableFallback(method.id, code.current);
} else {
await db.mfa.enable(method.id, code);
await db.mfa.enable(method.id, code.current);
}
const user = await db.user.fetchUser();
@@ -300,18 +229,14 @@ export const MFASetup = ({
setEnabling(false);
} catch (e) {
const error = e as Error;
formRef.current.setError("code", error.message);
ToastManager.error(error, "Error submitting 2fa code");
setEnabling(false);
}
};
const onSendCode = async () => {
if (error) return;
if (!method || sending) return;
if (method.id !== "app" && formRef.current.validateField("target")) {
return;
}
if (method.id === "app" && authenticatorDetails.sharedKey) {
Clipboard.setString(authenticatorDetails.sharedKey);
if (authenticatorDetails.authenticatorUri) {
@@ -329,37 +254,30 @@ export const MFASetup = ({
}
try {
const target = formRef.current.getValue("target").trim();
setGeneralError(undefined);
if (seconds) {
setGeneralError(strings.resendCodeWait());
return;
}
if (seconds) throw new Error(strings.resendCodeWait());
if (method.id === "sms" && !phoneNumber.current)
throw new Error(strings.phoneNumberNotEntered());
setSending(true);
await db.mfa.setup(method.id, method.id === "sms" ? target : undefined);
await db.mfa.setup(method?.id, phoneNumber.current);
if (method.id === "sms") {
setId(method.id + target);
setId(method.id + phoneNumber.current);
}
await sleep(300);
start(60, method.id === "sms" ? method.id + target : method.id);
start(
60,
method.id === "sms" ? method.id + phoneNumber.current : method.id
);
setSending(false);
ToastManager.show({
heading: strings["2faCodeSentVia"](method.id),
type: "success",
context: "local"
});
codeInputRef.current?.focus();
} catch (e) {
setSending(false);
const error = e as Error;
if (method.id === "sms" || method.id === "email") {
formRef.current.setError("target", error.message);
} else {
setGeneralError(error.message);
}
ToastManager.error(error, strings.errorSend2fa());
}
};
@@ -398,54 +316,55 @@ export const MFASetup = ({
</View>
) : (
<>
<FormInput
key={`${method.id}-${authenticatorDetails.sharedKey || user?.email || ""}`}
name="target"
formRef={formRef}
fwdRef={targetInputRef}
loading={method?.id !== "sms"}
editable={method.id === "sms"}
defaultValue={
method.id === "email"
? user?.email || ""
: method.id === "app"
? authenticatorDetails.sharedKey || ""
: undefined
<Input
loading={method?.id !== "sms" ? true : false}
value={
method?.id === "email"
? user?.email
: method?.id === "app"
? authenticatorDetails?.sharedKey || ""
: undefined
}
multiline={method.id === "app"}
onChangeText={() => {
setGeneralError(undefined);
onChangeText={(value) => {
phoneNumber.current = value;
}}
placeholder={
method.id === "email"
method?.id === "email"
? strings.enterEmailAddress()
: "+1234567890"
}
onSubmitEditing={onSendCode}
validators={targetValidators}
onSubmit={() => {
onSendCode();
}}
onErrorCheck={(e) => setError(e)}
validationType={method?.id === "email" ? "email" : "phonenumber"}
keyboardType={
method.id === "email" ? "email-address" : "phone-pad"
method.id == "email" ? "email-address" : "phone-pad"
}
errorMessage={
method?.id === "email"
? strings.enterValidEmail()
: strings.enterValidPhone()
}
buttons={
<Button
onPress={onSendCode}
loading={sending}
style={{
paddingVertical: 0,
paddingHorizontal: 0
}}
title={
sending
? null
: method.id === "app"
error ? null : (
<Button
onPress={onSendCode}
loading={sending}
title={
sending
? null
: method.id === "app"
? strings.copy()
: `${
seconds
? strings.resendCode(seconds as number)
: strings.sendCode()
}`
}
/>
}
/>
)
}
/>
@@ -454,22 +373,13 @@ export const MFASetup = ({
</Heading>
<Paragraph>{codeHelpText[method?.id]}</Paragraph>
<Seperator />
<FormInput
name="code"
formRef={formRef}
fwdRef={codeInputRef}
<Input
placeholder="xxxxxx"
maxLength={6}
loading={loading}
textAlign="center"
keyboardType="numeric"
onChangeText={() => {
setGeneralError(undefined);
}}
onSubmitEditing={onNext}
returnKeyLabel={strings.next()}
returnKeyType="done"
validators={codeValidators}
onChangeText={(value) => (code.current = value)}
inputStyle={{
fontSize: AppFontSize.lg,
height: 60,
@@ -482,25 +392,7 @@ export const MFASetup = ({
borderWidth: 0,
width: undefined
}}
errorStyle={{
textAlign: "center"
}}
/>
{generalError ? (
<Paragraph
size={AppFontSize.sm}
style={{
color: colors.error.icon,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
textAlign: "center",
width: "100%"
}}
>
{generalError}
</Paragraph>
) : null}
<Seperator />
<Button
title={enabling ? null : strings.next()}
@@ -625,7 +517,7 @@ export const MFARecoveryCodes = ({
ToastManager.show({
heading: strings.codesCopied(),
type: "success",
context: "local"
context: "global"
});
}}
style={{

View File

@@ -16,20 +16,15 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { db } from "../../../common/database";
import { Button } from "../../../components/ui/button";
import FormInput, {
createFormRef,
validators
} from "../../../components/ui/input/form-input";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { eUserLoggedIn } from "../../../utils/events";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import Input from "../../../components/ui/input";
import { Button } from "../../../components/ui/button";
enum EmailChangeSteps {
verify,
@@ -37,16 +32,14 @@ enum EmailChangeSteps {
}
export const ChangeEmail = () => {
const { colors } = useThemeColors();
const [step, setStep] = useState(EmailChangeSteps.verify);
const formRef = useRef(
createFormRef({
email: "",
password: "",
code: ""
})
);
const emailChangeData = useRef<{
email?: string;
password?: string;
code?: string;
}>({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const emailInputRef = useRef<TextInput>(null);
const passInputRef = useRef<TextInput>(null);
const codeInputRef = useRef<TextInput>(null);
@@ -54,52 +47,44 @@ export const ChangeEmail = () => {
const onSubmit = async () => {
try {
if (step === EmailChangeSteps.verify) {
const hasEmailError = formRef.current.validateField("email");
const hasPasswordError = formRef.current.validateField("password");
if (hasEmailError || hasPasswordError) return;
const { email, password } = formRef.current.getValues();
if (
!emailChangeData.current.email ||
!emailChangeData.current.password ||
error
)
return;
setLoading(true);
const verified = await db.user?.verifyPassword(password);
const verified = await db.user?.verifyPassword(
emailChangeData.current.password
);
if (!verified) throw new Error(strings.passwordIncorrect());
await db.user?.sendVerificationEmail(email);
await db.user?.sendVerificationEmail(emailChangeData.current.email);
setStep(EmailChangeSteps.changeEmail);
formRef.current.clearErrors();
formRef.current.setValue("code", "");
setLoading(false);
} else {
const hasCodeError = formRef.current.validateField("code");
if (hasCodeError) return;
const { email, password, code } = formRef.current.getValues();
setLoading(true);
await db.user?.changeEmail(email, password, code);
if (
!emailChangeData.current.email ||
!emailChangeData.current.password ||
error ||
!emailChangeData.current.code
)
return;
await db.user?.changeEmail(
emailChangeData.current.email,
emailChangeData.current.password,
emailChangeData.current.code
);
eSendEvent(eUserLoggedIn);
close?.();
ToastManager.show({
heading: strings.emailUpdated(email),
heading: strings.emailUpdated(emailChangeData.current.email),
type: "success",
context: "global"
});
Navigation.goBack();
setLoading(false);
}
} catch (e) {
setLoading(false);
const error = e as Error;
if (step === EmailChangeSteps.verify) {
if (error.message === strings.passwordIncorrect()) {
formRef.current.setError("password", error.message);
} else {
formRef.current.setError("email", error.message);
}
return;
} else {
formRef.current.setError("code", error.message);
return;
}
ToastManager.error(e as Error);
}
};
@@ -112,54 +97,34 @@ export const ChangeEmail = () => {
>
{step === EmailChangeSteps.verify ? (
<>
<FormInput
name="email"
formRef={formRef}
<Input
fwdRef={emailInputRef}
placeholder={strings.enterNewEmail()}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterValidEmail())
]}
onSubmitEditing={() => {
passInputRef.current?.focus();
validationType="email"
onErrorCheck={(e) => setError(e)}
onChangeText={(email) => {
emailChangeData.current.email = email;
}}
/>
<FormInput
name="password"
formRef={formRef}
<Input
fwdRef={passInputRef}
placeholder={strings.enterAccountPassword()}
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
autoComplete="password"
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={onSubmit}
onChangeText={(pass) => {
emailChangeData.current.password = pass;
}}
/>
</>
) : (
<>
<FormInput
name="code"
formRef={formRef}
<Input
key="code-input"
fwdRef={codeInputRef}
placeholder={strings.code()}
keyboardType="number-pad"
autoCapitalize="none"
autoCorrect={false}
maxLength={6}
validators={[
validators.required(strings.enterSixDigitCode()),
(value: string) =>
/^\d{6}$/.test(value.trim())
? undefined
: strings.enterSixDigitCode()
]}
onSubmitEditing={onSubmit}
defaultValue=""
onChangeText={(code) => {
emailChangeData.current.code = code;
}}
/>
</>
)}
@@ -170,8 +135,8 @@ export const ChangeEmail = () => {
loading
? undefined
: step === EmailChangeSteps.verify
? strings.verify()
: strings.changeEmail()
? strings.verify()
: strings.changeEmail()
}
type="accent"
style={{

View File

@@ -47,14 +47,6 @@ export async function verifyUser(
negativeText: closeText || strings.cancel(),
positivePress: async (value) => {
try {
if (!value || !value.trim()) {
ToastManager.error(
new Error(strings.passwordNotEntered()),
undefined,
"local"
);
return;
}
const user = await db.user.getUser();
let verified = !user ? true : await db.user.verifyPassword(value);
if (verified) {
@@ -103,14 +95,6 @@ export async function verifyUserWithApplock() {
keyboardType: keyboardType,
positivePress: async (value) => {
try {
if (!value || !value.trim()) {
ToastManager.error(
new Error(strings.passwordNotEntered()),
undefined,
"local"
);
return;
}
const verified = await validateAppLockPassword(value);
if (!verified) {
ToastManager.show({

View File

@@ -31,10 +31,11 @@ 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";
//@ts-ignore
import { enabled } from "react-native-privacy-snapshot";
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";
@@ -43,45 +44,43 @@ 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
presentSheet,
VaultRequestType
} 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[] = [
{
@@ -207,15 +206,15 @@ export const settingsGroups: SettingSection[] = [
return status === SubscriptionStatus.TRIAL
? strings.trialOnGoing(trialEndDate)
: status === SubscriptionStatus.ACTIVE
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
status === SubscriptionStatus.PAUSED
? strings.subEndsOn(expiryDate)
: status === SubscriptionStatus.EXPIRED
? subscriptionDaysLeft.time < -3
? strings.subEnded()
: strings.accountDowngradedIn(3)
: strings.neverHesitate();
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
status === SubscriptionStatus.PAUSED
? strings.subEndsOn(expiryDate)
: status === SubscriptionStatus.EXPIRED
? subscriptionDaysLeft.time < -3
? strings.subEnded()
: strings.accountDowngradedIn(3)
: strings.neverHesitate();
}
return strings.neverHesitate();
@@ -237,29 +236,18 @@ export const settingsGroups: SettingSection[] = [
presentDialog({
title: strings.redeemGiftCode(),
paragraph: strings.redeemGiftCodeDesc(),
form: {
formRef: createFormRef({
code: ""
}),
items: [
{
name: "code",
placeholder: strings.code(),
ref: React.createRef<TextInput | null>(),
validators: [validators.required(strings.giftCodeRequired())]
}
],
onFormSubmit: async (form) => {
try {
await db.subscriptions.redeemCode(form.getValue("code"));
return true;
} catch (e) {
form.setError("code", (e as Error).message);
return false;
}
}
},
positiveText: strings.redeem()
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"
});
});
}
});
}
},
@@ -529,14 +517,6 @@ export const settingsGroups: SettingSection[] = [
positiveText: strings.delete(),
positivePress: async (value) => {
try {
if (!value || !value.trim()) {
ToastManager.error(
new Error(strings.passwordNotEntered()),
undefined,
"local"
);
return;
}
const verified = await db.user?.verifyPassword(value);
if (verified) {
setTimeout(async () => {
@@ -1074,7 +1054,19 @@ export const settingsGroups: SettingSection[] = [
description: strings.privacyModeDesc(),
modifer: () => {
const settings = SettingsService.get();
SettingsService.setPrivacyScreen(!settings.privacyScreen);
if (Platform.OS === "ios") {
enabled(!settings.privacyScreen);
if (settings.privacyScreen) {
ScreenGuardModule.unregister();
} else {
ScreenGuardModule.register({
backgroundColor: "#000000"
});
}
} else {
NotesnookModule.setSecureMode(!settings.privacyScreen);
}
SettingsService.set({ privacyScreen: !settings.privacyScreen });
},
property: "privacyScreen"
@@ -1095,13 +1087,11 @@ export const settingsGroups: SettingSection[] = [
type: "switch",
property: "appLockEnabled",
featureId: "appLock",
onChange: (property) => {
if (property) {
SettingsService.set({
privacyScreen: true
});
SettingsService.setPrivacyScreen(true);
}
onChange: () => {
SettingsService.set({
privacyScreen: true
});
SettingsService.setPrivacyScreen(SettingsService.get());
},
onVerify: async () => {
const verified = await verifyUserWithApplock();

View File

@@ -201,14 +201,6 @@ const SettingsUserSection = ({ item }) => {
inputPlaceholder: strings.enterFullName(),
defaultValue: userProfile?.fullName,
positivePress: async (value) => {
if (!value || !value.trim()) {
ToastManager.error(
new Error(strings.nameIsRequired()),
undefined,
"local"
);
return;
}
db.settings
.setProfile({
fullName: value

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,9 @@ 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";
//@ts-ignore
import { enabled } from "react-native-privacy-snapshot";
import { MMKV } from "../common/database/mmkv";
import {
SettingStore,
@@ -29,7 +31,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();
@@ -53,10 +54,11 @@ function resetSettings() {
defaultSnoozeTime: get().defaultSnoozeTime,
defaultFontFamily: get().defaultFontFamily,
defaultFontSize: get().defaultFontSize,
privacyScreen: get().privacyScreen,
corsProxy: get().corsProxy,
showBackupCompleteSheet: get().showBackupCompleteSheet
};
setPrivacyScreen(false);
MMKV.setString("appSettings", JSON.stringify(settings));
set(settings);
init();
@@ -99,7 +101,6 @@ function migrateSettings(settings: SettingStore["settings"]) {
settings.privacyScreen = settings.appLockEnabled
? true
: settings.privacyScreen;
setPrivacyScreen(settings.privacyScreen);
MMKV.setString("appSettings", JSON.stringify(settings));
}
}
@@ -125,21 +126,28 @@ function init() {
updateSize();
useSettingStore.getState().setSettings({ ...settings });
migrateAppLock();
setPrivacyScreen(settings);
}
function setPrivacyScreen(enabled?: boolean) {
if (enabled) {
function setPrivacyScreen(settings: SettingStore["settings"]) {
if (settings.privacyScreen) {
if (Platform.OS === "android") {
NotesnookModule.setSecureMode(true);
ScreenGuardModule.registerWithoutEffect();
} else {
ScreenGuardModule.register({ backgroundColor: "#000000" });
enabled(true);
if (NativeModules.ScreenGuard) {
ScreenGuardModule.register({ backgroundColor: "#000000" });
}
}
} else {
if (Platform.OS === "android") {
NotesnookModule.setSecureMode(false);
} else {
enabled(false);
if (NativeModules.ScreenGuard) {
ScreenGuardModule.unregister();
}
}
ScreenGuardModule.unregister();
}
}

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

@@ -65,14 +65,6 @@ export async function unlockVault({
paragraph: paragraph,
inputPlaceholder: strings.enterPassword(),
positivePress: async (value) => {
if (!value || !value.trim()) {
ToastManager.error(
new Error(strings.passwordNotEntered()),
undefined,
"local"
);
return;
}
const unlocked = await db.vault.unlock(value);
if (!unlocked) {
ToastManager.show({

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2183
IOS_MARKETING_VERSION = 3.3.23
IOS_CURRENT_PROJECT_VERSION = 2180
IOS_MARKETING_VERSION = 3.3.20
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 = 2183
IOS_MARKETING_VERSION = 3.3.23
IOS_CURRENT_PROJECT_VERSION = 2180
IOS_MARKETING_VERSION = 3.3.20
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 = 2183
IOS_MARKETING_VERSION = 3.3.23
IOS_CURRENT_PROJECT_VERSION = 2180
IOS_MARKETING_VERSION = 3.3.20
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.23",
"version": "3.3.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.3.23",
"version": "3.3.19",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -105,6 +105,7 @@
"react-native-notification-sounds": "0.5.5",
"react-native-orientation-locker": "^1.7.0",
"react-native-pdf": "^7.0.3",
"react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot",
"react-native-progress": "5.0.0",
"react-native-qrcode-svg": "^6.0.6",
"react-native-quick-sqlite": "^8.2.7",
@@ -112,7 +113,7 @@
"react-native-reanimated-progress-bar": "1.0.1",
"react-native-safe-area-context": "^5.6.1",
"react-native-scoped-storage": "^1.9.5",
"react-native-screenguard": "^2.0.0-beta5",
"react-native-screenguard": "1.0.0",
"react-native-screens": "^4.19.0",
"react-native-securerandom": "^1.0.1",
"react-native-share": "^12.0.3",
@@ -15883,6 +15884,15 @@
"prop-types": "*"
}
},
"node_modules/react-native-privacy-snapshot": {
"version": "1.0.0",
"resolved": "git+ssh://git@github.com/standardnotes/react-native-privacy-snapshot.git#653e904c90fc6f2b578da59138f2bfe5d7f942fe",
"license": "MIT",
"peerDependencies": {
"react": "^16.13.1",
"react-native": "^0.62.0"
}
},
"node_modules/react-native-progress": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-native-progress/-/react-native-progress-5.0.0.tgz",
@@ -15983,23 +15993,16 @@
}
},
"node_modules/react-native-screenguard": {
"version": "2.0.0-beta5",
"resolved": "https://registry.npmjs.org/react-native-screenguard/-/react-native-screenguard-2.0.0-beta5.tgz",
"integrity": "sha512-v8Lnv2am+COzEGKvj6J4UbpQsjHixkSqSJ+tgJtDvwck2Dil1vwOhEJBbngXYgj63x4iFlV9gE4gAOWG60avAg==",
"hasInstallScript": true,
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-native-screenguard/-/react-native-screenguard-1.0.0.tgz",
"integrity": "sha512-fqtoJI8TxszKqrntnHW48EMb4gNVA9VCAg5UG1FcIXEyv2W70kWoMxRRFoWuCRYlBWSAWqiXO+R6Xfk/0um0cw==",
"license": "MIT",
"engines": {
"node": ">= 16.0.0"
},
"peerDependencies": {
"expo": ">=36.0.0",
"react": ">=16.8.3",
"react-native": ">=0.59.0"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-screens": {

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.3.23",
"version": "3.3.20",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {
@@ -123,6 +123,7 @@
"react-native-notification-sounds": "0.5.5",
"react-native-orientation-locker": "^1.7.0",
"react-native-pdf": "^7.0.3",
"react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot",
"react-native-progress": "5.0.0",
"react-native-qrcode-svg": "^6.0.6",
"react-native-quick-sqlite": "^8.2.7",
@@ -130,7 +131,7 @@
"react-native-reanimated-progress-bar": "1.0.1",
"react-native-safe-area-context": "^5.6.1",
"react-native-scoped-storage": "^1.9.5",
"react-native-screenguard": "^2.0.0-beta5",
"react-native-screenguard": "1.0.0",
"react-native-screens": "^4.19.0",
"react-native-securerandom": "^1.0.1",
"react-native-share": "^12.0.3",
@@ -201,4 +202,4 @@
"engines": {
"node": ">=20"
}
}
}

View File

@@ -0,0 +1,54 @@
diff --git a/node_modules/react-native-screenguard/ios/ScreenGuard.mm b/node_modules/react-native-screenguard/ios/ScreenGuard.mm
index 9fa7d33..d2e6d68 100644
--- a/node_modules/react-native-screenguard/ios/ScreenGuard.mm
+++ b/node_modules/react-native-screenguard/ios/ScreenGuard.mm
@@ -11,7 +11,7 @@
@implementation ScreenGuard
RCT_EXPORT_MODULE(ScreenGuard)
-bool hasListeners;
+bool hasListeners_;
UITextField *textField;
UIImageView *imageView;
@@ -22,11 +22,11 @@ @implementation ScreenGuard
}
- (void)startObserving {
- hasListeners = YES;
+ hasListeners_ = YES;
}
- (void)stopObserving {
- hasListeners = NO;
+ hasListeners_ = NO;
}
- (void)secureViewWithBackgroundColor: (NSString *)color {
@@ -335,7 +335,7 @@ - (UIImage *)convertViewToImage:(UIView *)view {
queue:mainQueue
usingBlock:^(NSNotification *notification) {
- if (hasListeners && getScreenShotPath) {
+ if (hasListeners_ && getScreenShotPath) {
UIViewController *presentedViewController = RCTPresentedViewController();
UIImage *image = [self convertViewToImage:presentedViewController.view.superview];
@@ -359,7 +359,7 @@ - (UIImage *)convertViewToImage:(UIView *)view {
result = @{@"path": filePath, @"name": fileName, @"type": @"PNG"};
}
[self emit:SCREENSHOT_EVT body: result];
- } else if (hasListeners) {
+ } else if (hasListeners_) {
[self emit:SCREENSHOT_EVT body: nil];
}
}];
@@ -376,7 +376,7 @@ - (UIImage *)convertViewToImage:(UIView *)view {
queue:mainQueue
usingBlock:^(NSNotification *notification) {
- if (hasListeners) {
+ if (hasListeners_) {
[self emit:SCREEN_RECORDING_EVT body:nil];
}
}];

View File

@@ -11,6 +11,12 @@ config.dependencies["react-native-vector-icons"] = {
}
};
config.dependencies["react-native-screenguard"] = {
platforms: {
android: null
}
};
if (isGithubRelease) {
config.dependencies["react-native-iap"] = {
platforms: {

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "3.3.18",
"version": "3.3.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "3.3.18",
"version": "3.3.14",
"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.18",
"version": "3.3.14",
"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

@@ -231,40 +231,15 @@ export async function restoreBackupFile(backupFile: File) {
}
await db.initCollections();
} else {
const { createUnzipIterator } = await import(
"../utils/streams/unzip-stream"
);
let skipAttachments = false;
if (!useUserStore.getState().isLoggedIn) {
let hasAttachments = false;
for await (const entry of createUnzipIterator(backupFile)) {
if (
entry.name.startsWith("attachments/") &&
entry.name !== "attachments/.attachments_key"
) {
hasAttachments = true;
break;
}
}
if (hasAttachments) {
const result = await ConfirmDialog.show({
title: strings.loginToRestoreAttachments(),
message: strings.loginToRestoreAttachmentsDesc(),
positiveButtonText: strings.yes(),
negativeButtonText: strings.no()
});
if (!result) return;
skipAttachments = true;
}
}
const error = await TaskManager.startTask<Error | void>({
title: strings.restoringBackup(),
subtitle: strings.restoringBackupDesc(),
type: "modal",
action: async (report) => {
const { createUnzipIterator } = await import(
"../utils/streams/unzip-stream"
);
let cachedPassword: string | undefined = undefined;
let cachedKey: string | undefined = undefined;
// const { read, totalFiles } = await Reader(backupFile);
@@ -279,13 +254,13 @@ export async function restoreBackupFile(backupFile: File) {
isValid = true;
continue;
}
if (!skipAttachments && entry.name === "attachments/.attachments_key")
if (entry.name === "attachments/.attachments_key")
attachmentsKey = JSON.parse(await entry.text()) as
| SerializedKey
| Cipher<"base64">;
else if (!skipAttachments && entry.name.startsWith("attachments/"))
else if (entry.name.startsWith("attachments/"))
attachments.push(entry);
else if (!entry.name.startsWith("attachments/")) entries.push(entry);
else entries.push(entry);
}
if (!isValid)
console.warn(
@@ -364,8 +339,6 @@ export async function restoreBackupFile(backupFile: File) {
if (error) {
console.error(error);
showToast("error", `${strings.restoreFailed()}: ${error.message}`);
} else {
showToast("success", strings.backupRestored());
}
}
}

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

@@ -93,7 +93,6 @@ export function DayPicker(props: DayPickerProps) {
key={month.month + year}
value={month.month}
data-date={month.$date.toDateString()}
disabled={month.disabled}
>
{month.month}
</option>
@@ -122,7 +121,6 @@ export function DayPicker(props: DayPickerProps) {
key={year.year}
value={year.year}
data-date={year.$date.toDateString()}
disabled={year.disabled}
>
{year.year}
</option>

View File

@@ -324,7 +324,6 @@ function GroupHeader(props: GroupHeaderProps) {
cursor: "pointer",
px: 1,
py: 1,
pr: 2,
borderBottom: "1px solid var(--border)",
// border: isMenuTarget ? "1px solid" : "none",
// borderColor: isMenuTarget ? "accent" : "transparent",

View File

@@ -55,8 +55,6 @@ export const CustomScrollbarsVirtualList = forwardRef<
return (
<ScrollContainer
{...props}
trackStyle={() => ({ width: 6 })}
thumbStyle={() => ({ width: 6 })}
forwardedRef={(sRef) => {
if (typeof ref === "function") ref(sRef);
else if (ref) ref.current = sRef;

View File

@@ -148,7 +148,6 @@ function ListItem<TItem extends Item, TContext>(
sx={{
px: 1,
py: isCompact ? 0 : 1,
pr: isCompact ? 10 : 0,
height: isCompact ? 25 : "inherit",
cursor: "pointer",
position: "relative",

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

@@ -104,7 +104,6 @@ import ListItem from "../list-item";
import { PublishDialog } from "../publish-view";
import TimeAgo from "../time-ago";
import { NoteExpiryDateDialog } from "../../dialogs/note-expiry-date-dialog";
import { withFeatureCheck } from "../../common";
type NoteProps = NoteResolvedData & {
item: NoteType;
@@ -666,11 +665,11 @@ export const noteMenuItems: (
title: strings.setExpiry(),
icon: Destruct.path,
premium: !features.expiringNotes.isAllowed,
onClick: withFeatureCheck(features.expiringNotes, async () => {
onClick: async () => {
await NoteExpiryDateDialog.show({
noteId: note.id
});
})
}
},
{
type: "button",
@@ -714,7 +713,7 @@ function colorsToMenuItems(
const isChecked = !!noteColor && noteColor.id === color.id;
return {
type: "button",
key: color.id,
key: color.title,
title: color.title,
icon: Circle.path,
styles: { icon: { color: color.colorCode } },

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

@@ -68,23 +68,13 @@ export function getWindowControls(
];
}
export const TITLE_BAR_HEIGHT = 37;
export function TitleBar({
isUnderlay = isMac(),
force = false
}: {
isUnderlay?: boolean;
force?: boolean;
}) {
export function TitleBar({ isUnderlay = isMac() }: { isUnderlay?: boolean }) {
const { isFullscreen, hasNativeWindowControls, isMaximized } =
useWindowControls();
const isTablet = useTablet();
const isMobile = useMobile();
if (
!force &&
((!isMac() && !isMobile && !isTablet) || (isFullscreen && isMac()))
) {
if ((!isMac() && !isMobile && !isTablet) || (isFullscreen && isMac()))
return null;
}
const tools = getWindowControls(
hasNativeWindowControls,

View File

@@ -44,7 +44,7 @@ export const AddNotebookDialog = DialogManager.register(
const onSubmit = useCallback(async () => {
if (!title.current.trim())
return showToast("error", strings.titleIsRequired());
return showToast("error", strings.allFieldsRequired());
const id = await db.notebooks.add({
id: props.notebook?.id,

View File

@@ -128,10 +128,7 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
return;
}
if (
!callback_data?.data ||
SUBSCRIBED_EVENTS.indexOf(event_name) === -1
) {
if (!callback_data.data || SUBSCRIBED_EVENTS.indexOf(event_name) === -1) {
return;
}

View File

@@ -47,7 +47,7 @@ export interface Checkout {
export type PaddleEvent = {
action: "event";
event_name: CheckoutEventNames;
callback_data?: PaddleEventData;
callback_data: PaddleEventData;
};
export type PlanMetadata = Record<

View File

@@ -25,7 +25,7 @@ import { writeText } from "clipboard-polyfill";
import { store as userstore } from "../stores/user-store";
import { ErrorText } from "../components/error-text";
import { Debug, IssueReportResponse } from "@notesnook/core";
import { Debug } from "@notesnook/core";
import { ConfirmDialog } from "./confirm";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
@@ -76,16 +76,15 @@ export const IssueDialog = DialogManager.register(function IssueDialog(
if (!requestData.title.trim() || !requestData.body.trim()) return;
requestData.body = BODY_TEMPLATE(requestData.body);
const response = await Debug.report({
const url = await Debug.report({
title: requestData.title,
body: requestData.body,
userId: userstore.get().user?.id
});
if (!response) throw new Error("Could not submit bug report.");
if ("error" in response) throw new Error(response.error);
if (!url) throw new Error("Could not submit bug report.");
props.onClose(true);
await showIssueReportedDialog(response);
await showIssueReportedDialog({ url });
} catch (e) {
if (e instanceof Error) setError(e.message);
} finally {
@@ -158,37 +157,14 @@ export const IssueDialog = DialogManager.register(function IssueDialog(
);
});
function showIssueReportedDialog(response: IssueReportResponse) {
if ("error" in response) return;
switch (response.type) {
case "email": {
return ConfirmDialog.show({
title: strings.yourSupportRequestHasBeenForwarded(),
message: strings.supportEmailMessage()
});
}
case "discussion": {
const url = response.url;
return ConfirmDialog.show({
title: strings.thankYouForFeedback(),
positiveButtonText: strings.copyLink(),
message: strings.featureRequestMessage(url)
}).then((result) => {
result && writeText(url);
});
}
case "issue": {
const url = response.url;
return ConfirmDialog.show({
title: strings.thankYouForReporting(),
positiveButtonText: strings.copyLink(),
message: strings.bugReportMessage(url)
}).then((result) => {
result && writeText(url);
});
}
}
function showIssueReportedDialog({ url }: { url: string }) {
return ConfirmDialog.show({
title: strings.thankYouForReporting(),
positiveButtonText: strings.copyLink(),
message: strings.bugReportMessage(url)
}).then((result) => {
result && writeText(url);
});
}
const BODY_TEMPLATE = (body: string) => {

View File

@@ -32,7 +32,6 @@ import {
getFeaturesUsage,
usePromise
} from "@notesnook/common";
import { SubscriptionProvider } from "@notesnook/core";
export function SubscriptionStatus() {
const user = useUserStore((store) => store.user);
@@ -45,8 +44,6 @@ export function SubscriptionStatus() {
? ""
: trial
? `Your free trial is on-going. Your subscription will start on ${trialExpiryDate}.`
: user?.subscription.provider === SubscriptionProvider.GIFT_CARD
? `Your subscription will expire on ${expiryDate}.`
: autoRenew
? `Your subscription will auto renew on ${expiryDate}.`
: expiryDate

View File

@@ -47,7 +47,6 @@ import { FlexScrollContainer } from "../../components/scroll-container";
import { useCallback, useEffect, useRef, useState } from "react";
import {
DropdownSettingComponent,
Section,
SectionGroup,
SectionKeys,
Setting,
@@ -255,15 +254,9 @@ export const SettingsDialog = DialogManager.register(function SettingsDialog(
overflow: "auto"
}}
>
{activeSettings.length > 0 ? (
activeSettings.map((group) => (
<SettingsGroupComponent item={group} />
))
) : (
<Text variant="body" sx={{ color: "paragraph-secondary" }}>
{strings.noResultsFound()}
</Text>
)}
{activeSettings.map((group) => (
<SettingsGroupComponent item={group} />
))}
</FlexScrollContainer>
</Flex>
</Dialog>
@@ -324,17 +317,12 @@ function SettingsSideBar(props: SettingsSideBarProps) {
SettingsGroups.filter((g) => g.section === route)
);
let groups: SettingsGroup[] = [];
const groups: SettingsGroup[] = [];
for (const group of SettingsGroups) {
const section = findSection(group.section);
if (section?.isHidden?.() || group.isHidden?.()) continue;
const isTitleMatch =
typeof group.header === "string" &&
group.header.toLowerCase().includes(query);
const isSectionMatch = group.section
.toLowerCase()
.includes(query);
const isSectionMatch = group.section.includes(query);
if (isTitleMatch || isSectionMatch) {
groups.push(group);
@@ -359,18 +347,6 @@ function SettingsSideBar(props: SettingsSideBarProps) {
if (!settings.length) continue;
groups.push({ ...group, settings });
}
const matchedSections = findSections(query);
if (matchedSections.length > 0) {
const matchedGroups = SettingsGroups.filter((g) =>
matchedSections.some((s) => s.key === g.section)
);
// remove groups whose sections were matched to avoid duplicate
// entries.
groups = groups.filter(
(g) => !matchedGroups.some((mg) => mg.section === g.section)
);
groups.push(...matchedGroups);
}
onNavigate(groups);
}}
/>
@@ -741,22 +717,3 @@ function NumberInput({
</Flex>
);
}
function findSection(key: SectionKeys) {
for (const group of sectionGroups) {
const section = group.sections.find((s) => s.key === key);
if (section) return section;
}
return null;
}
function findSections(query: string) {
const sections: Section[] = [];
for (const group of sectionGroups) {
for (const section of group.sections) {
if (section.isHidden?.()) continue;
if (section.title.toLowerCase().includes(query)) sections.push(section);
}
}
return sections;
}

View File

@@ -49,7 +49,6 @@ import {
} from "@notesnook/core";
import { logger } from "../utils/logger";
import { newQueue } from "@henrygd/queue";
import { strings } from "@notesnook/intl";
export const ABYTES = 17;
const CHUNK_SIZE = 512 * 1024;
@@ -500,20 +499,12 @@ async function downloadFile(
{ type: "download", hash: filename }
);
const signedUrlResponse = await axios
.get(url, { headers, responseType: "text" })
.catch((e) => {
if (e.response?.status === 401) {
showToast("error", strings.pleaseLoginToDownloadAttachments());
return null;
}
throw e;
});
if (!signedUrlResponse) {
reportProgress(undefined, { type: "download", hash: filename });
return false;
}
const signedUrl = signedUrlResponse.data;
const signedUrl = (
await axios.get(url, {
headers,
responseType: "text"
})
).data;
logger.debug("Got attachment signed url", { filename });

View File

@@ -1,57 +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 { getCurrentPath } from "./index";
export type AuthRoutes =
| "sessionExpiry"
| "login:email"
| "login:password"
| "signup"
| "recover"
| "mfa:code"
| "mfa:select";
export const authRoutes: Record<AuthRoutes, string> = {
"login:email": "/login",
"login:password": "/login/password",
"mfa:code": "/login/mfa/code",
"mfa:select": "/login/mfa/select",
recover: "/recover",
sessionExpiry: "/sessionexpired",
signup: "/signup"
};
const unauthorizedRoutes: AuthRoutes[] = [
"login:email",
"login:password",
"signup",
"mfa:code",
"mfa:select",
"recover"
];
export function isAuthRoute() {
const path = getCurrentPath();
return Object.values(authRoutes).includes(path);
}
export function isUnauthorizedRoute(route: AuthRoutes) {
return unauthorizedRoutes.includes(route);
}

View File

@@ -23,6 +23,7 @@ import Notes from "../views/notes";
import { NotebookHeader } from "../components/notebook-header";
import Trash from "../views/trash";
import { useStore as useNoteStore } from "../stores/note-store";
import { useStore as useAppStore } from "../stores/app-store";
import Reminders from "../views/reminders";
import { RouteResult, defineRoutes } from "./types";
import { CREATE_BUTTON_MAP } from "../common";

View File

@@ -34,7 +34,6 @@ import AppLock from "./views/app-lock";
import { Text } from "@theme-ui/components";
import { EV, EVENTS } from "@notesnook/core";
import { useEffect, useState } from "react";
import { isAuthRoute } from "./navigation/auth-routes";
export async function startApp(children?: React.ReactNode) {
const rootElement = document.getElementById("root");
@@ -58,7 +57,7 @@ export async function startApp(children?: React.ReactNode) {
root.render(
<>
<TitleBar force={isAuthRoute()} />
<TitleBar />
<ErrorBoundary>
<GlobalErrorHandler>
<BaseThemeProvider

View File

@@ -44,11 +44,6 @@ import { showLogoutConfirmation } from "../dialogs/confirm";
import { TaskManager } from "../common/task-manager";
import { strings } from "@notesnook/intl";
import { ScrollContainer } from "@notesnook/ui";
import {
authRoutes,
AuthRoutes,
isUnauthorizedRoute
} from "../navigation/auth-routes";
type EmailFormData = {
email: string;
@@ -117,7 +112,14 @@ type BaseAuthComponentProps<TRoute extends AuthRoutes> = {
navigate: NavigateFunction;
formData?: AuthFormData[TRoute];
};
type AuthRoutes =
| "sessionExpiry"
| "login:email"
| "login:password"
| "signup"
| "recover"
| "mfa:code"
| "mfa:select";
export type AuthProps = {
route: AuthRoutes;
isolated?: boolean;
@@ -151,6 +153,25 @@ function getRouteComponent<TRoute extends AuthRoutes>(
return undefined;
}
const routePaths: Record<AuthRoutes, string> = {
"login:email": "/login",
"login:password": "/login/password",
"mfa:code": "/login/mfa/code",
"mfa:select": "/login/mfa/select",
recover: "/recover",
sessionExpiry: "/sessionexpired",
signup: "/signup"
};
const authorizedRoutes: AuthRoutes[] = [
"login:email",
"login:password",
"signup",
"mfa:code",
"mfa:select",
"recover"
];
function Auth(props: AuthProps) {
return (
<AuthContainer>
@@ -170,12 +191,12 @@ export function HeadlessAuth(props: AuthProps) {
const Route = useMemo(() => getRouteComponent(route), [route]);
useEffect(() => {
if (isolated) return;
window.history.replaceState({}, "", makeURL(authRoutes[route]));
window.history.replaceState({}, "", makeURL(routePaths[route]));
}, [route, isolated]);
useEffect(() => {
db.user.getUser().then((user) => {
if (user && isUnauthorizedRoute(route) && !isSessionExpired())
if (user && authorizedRoutes.includes(route) && !isSessionExpired())
return openURL("/", { authenticated: true });
performance.mark("load:auth");
setIsReady(true);

View File

@@ -1,8 +0,0 @@
---
title: Login to restore attachments in backup.
description: We require users to be logged in to restore attachments in backup.
---
# Login to restore attachments in backup.
We require users to be logged in to restore attachments in backup. This is because attachments are encrypted using a sub-key derived from your database encryption key. Without a login, we cannot encrypt/upload/sync attachments.

View File

@@ -87,4 +87,3 @@ navigation:
- path: faqs/what-are-merge-conflicts.md
- path: faqs/is-there-an-eta.md
- path: faqs/login-to-upload-attachments.md
- path: faqs/login-to-restore-attachments-in-backup.md

View File

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

View File

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

View File

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

View File

@@ -234,18 +234,6 @@ test("note should get headline from first paragraph in content", () =>
expect(note?.headline).toBe("This is a very colorful existence.");
}));
test("note headline should be capped at 350 characters", () =>
noteTest({
content: {
type: TEST_NOTE.content.type,
data: `<p>${"a".repeat(400)}</p>`
}
}).then(async ({ db, id }) => {
const note = await db.notes.note(id);
expect(note?.headline).toHaveLength(350);
expect(note?.headline).toBe("a".repeat(350));
}));
test("note should not get headline if there is no p tag", () =>
noteTest({
...TEST_NOTE,

View File

@@ -19,24 +19,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import hosts from "../utils/constants.js";
export type IssueReportResponse =
| { error: string }
| { url: string; type: "issue" | "discussion" }
| { type: "email" };
export class Debug {
static async report(reportData: {
title: string;
body: string;
userId?: string;
}): Promise<IssueReportResponse | undefined> {
}): Promise<string | undefined> {
const { title, body, userId } = reportData;
const response = await fetch(`${hosts.ISSUES_HOST}/create/notesnook`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, body, userId })
});
if (!response.ok) return;
const json = await response.json();
if (!response.ok) return json;
return json;
return json.url;
}
}

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

@@ -548,24 +548,18 @@ class UserManager {
const email = newEmail.toLowerCase();
try {
await http.patch(
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
{
type: "change_email",
new_email: newEmail,
password: await this.db.storage().hash(password, email, {
usesFallback: await this.usesFallbackPWHash(password)
}),
verification_code: code
},
token
);
} catch (e) {
const error = e as Error;
if (error.message === "Invalid token.") throw new Error("Invalid code.");
throw error;
}
await http.patch(
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
{
type: "change_email",
new_email: newEmail,
password: await this.db.storage().hash(password, email, {
usesFallback: await this.usesFallbackPWHash(password)
}),
verification_code: code
},
token
);
}
recoverAccount(email: string) {

View File

@@ -90,7 +90,7 @@ export class Tiptap {
}
toHeadline() {
return extractHeadline(this.data, 350);
return extractHeadline(this.data);
}
toTitle() {

View File

@@ -588,8 +588,7 @@ export enum SubscriptionProvider {
STREETWRITERS = 0,
APPLE = 1,
GOOGLE = 2,
PADDLE = 3,
GIFT_CARD = 4
PADDLE = 3
}
export type User = {

View File

@@ -44,7 +44,7 @@ function wrapIntoHTMLDocument(input: string) {
return `<!doctype html><html lang="en"><head><title>Document Fragment</title></head><body>${input}</body></html>`;
}
export function extractHeadline(html: string, characterLimit: number) {
export function extractHeadline(html: string) {
let text = "";
let start = false;
const parser = new Parser(
@@ -60,14 +60,7 @@ export function extractHeadline(html: string, characterLimit: number) {
}
},
ontext: (data) => {
if (start) {
text += data
if (text.length > characterLimit){
text = text.slice(0, characterLimit);
parser.pause()
parser.end()
}
}
if (start) text += data;
}
},
{

Some files were not shown because too many files have changed in this diff Show More