mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 19:19:34 +02:00
Compare commits
1 Commits
3.3.23-and
...
mobile/rel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0ce43a552 |
51
.github/workflows/desktop.preview.yml
vendored
51
.github/workflows/desktop.preview.yml
vendored
@@ -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`;
|
||||
|
||||
@@ -198,9 +198,6 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
},
|
||||
toolsets: {
|
||||
appimage: "1.0.2"
|
||||
},
|
||||
snap: {
|
||||
autoStart: false,
|
||||
confinement: "strict",
|
||||
|
||||
2128
apps/desktop/package-lock.json
generated
2128
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 }} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -136,7 +136,7 @@ const BaseDialog = ({
|
||||
? background
|
||||
: transparent
|
||||
? "transparent"
|
||||
: "rgba(0,0,0,0.1)"
|
||||
: "rgba(0,0,0,0.3)"
|
||||
}}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { 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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -181,7 +181,6 @@ export default function ReminderNotify({
|
||||
data={references}
|
||||
loading={false}
|
||||
dataType="note"
|
||||
groupType="notes"
|
||||
isRenderedInActionSheet={true}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ export const Archive = ({ navigation, route }: NavigationProps<"Archive">) => {
|
||||
onRefresh={() => {
|
||||
refresh();
|
||||
}}
|
||||
groupType="archive"
|
||||
renderedInRoute="Archive"
|
||||
loading={loading}
|
||||
placeholder={{
|
||||
|
||||
@@ -70,7 +70,6 @@ export const Favorites = ({
|
||||
<List
|
||||
data={favorites}
|
||||
dataType="note"
|
||||
groupType="favorites"
|
||||
onRefresh={() => {
|
||||
refresh();
|
||||
}}
|
||||
|
||||
@@ -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]()}
|
||||
|
||||
@@ -185,7 +185,6 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
<List
|
||||
data={notes}
|
||||
dataType="note"
|
||||
groupType="notes"
|
||||
onRefresh={() => {
|
||||
onRequestUpdate();
|
||||
}}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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({
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
33
apps/mobile/package-lock.json
generated
33
apps/mobile/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
54
apps/mobile/patches/react-native-screenguard+1.0.0.patch
Normal file
54
apps/mobile/patches/react-native-screenguard+1.0.0.patch
Normal 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];
|
||||
}
|
||||
}];
|
||||
@@ -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: {
|
||||
|
||||
4
apps/web/package-lock.json
generated
4
apps/web/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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 : (
|
||||
|
||||
BIN
apps/web/src/assets/testimonials/grberk.jpeg
Normal file
BIN
apps/web/src/assets/testimonials/grberk.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
- Bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -1,3 +0,0 @@
|
||||
- Bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -1,3 +0,0 @@
|
||||
- Bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: [] };
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -90,7 +90,7 @@ export class Tiptap {
|
||||
}
|
||||
|
||||
toHeadline() {
|
||||
return extractHeadline(this.data, 350);
|
||||
return extractHeadline(this.data);
|
||||
}
|
||||
|
||||
toTitle() {
|
||||
|
||||
@@ -588,8 +588,7 @@ export enum SubscriptionProvider {
|
||||
STREETWRITERS = 0,
|
||||
APPLE = 1,
|
||||
GOOGLE = 2,
|
||||
PADDLE = 3,
|
||||
GIFT_CARD = 4
|
||||
PADDLE = 3
|
||||
}
|
||||
|
||||
export type User = {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user