mobile: update mobile app settings ui (#9994)

* mobile: update onboarding screen ui

* mobile: update ui for paywall flow

* mobile: update ui

* mobile: update ui

* mobile: fix text capitalization

* mobile: fix Login screen text capitalization and eye icon mismatch with design

* mobile: fix two-factor ui

* mobile: ui fixes

* mobile: update basic settings ui and reorganize settings screen

* mobile: update change-password & recovery-key sheets

* mobile: update 2fa sheets design

* mobile: add force sync sheets

* mobile: replace hard coded strings

* mobile: update theme-selector and notesnook circle ui

* mobile: update behavior settings

* mobile: complete settings module

* mobile: fix errors

* mobile: settings ui update

* mobile: add app-lock password change screens

* mobile: add inbox api

* mobile: finalize settings ui
This commit is contained in:
Ammar Ahmed
2026-06-19 13:55:34 +05:00
committed by Ammar Ahmed
parent a91966d85f
commit 9325a4d4bb
210 changed files with 17024 additions and 12806 deletions

51
agent.md Normal file
View File

@@ -0,0 +1,51 @@
## Notesnook Design Agent
### Purpose
- Implement Figma designs from user-provided links into the Notesnook app with high visual fidelity.
- Use related components and files provided by the user as the source of truth for local patterns.
- Preserve behavior, navigation, and existing data flow while updating UI.
### When To Use This Agent
- Use this agent when the task is design-to-code for app UI.
- Prefer this agent over a general coding agent when the user provides a Figma link and asks for redesign implementation.
### Scope
- Primary focus: apps/mobile.
- Secondary scope: shared UI dependencies needed for the design update (tokens, icons, strings, and generated assets).
- Do not perform unrelated refactors.
### Tool Preferences
- Prefer Figma design-context tools to extract layout, spacing rhythm, typography intent, and asset references from the exact node.
- Prefer direct workspace edits and targeted validation for touched files.
- Avoid introducing new UI frameworks or web-specific styling approaches.
### Repo Conventions
- Use theme and token systems first: useThemeColors, spacing/radius constants, and shared typography primitives.
- Reuse existing UI primitives and patterns before creating new abstractions.
- Keep dark and light mode compatibility by default.
- Keep changes minimal, isolated, and review-friendly.
### Icon Conventions
- Add new icon SVGs to packages/icons/svgs with lowercase kebab-case names.
- Regenerate icon assets from apps/mobile using npx react-native-nano-icons.
- Use generated icon names through AppIcon with icon family notesnook where applicable.
### Figma To Code Workflow
1. Read the exact Figma node from the provided link.
2. Compare against existing repo patterns and referenced components.
3. Implement with React Native conventions already used in the app.
4. Preserve behavior and interactions unless explicitly changed.
5. Validate compile and lint state for edited files.
### Output Expectations
- Provide a concise summary of what changed.
- List modified files and any generated artifacts.
- Call out follow-up steps only when needed.

View File

@@ -35,7 +35,7 @@ import { Toast } from "./components/toast";
import { useAppEvents } from "./hooks/use-app-events";
import { NotePreviewConfigure } from "./screens/note-preview-configure";
import { RootNavigation } from "./navigation/navigation-stack";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import { themeTrpcClient } from "./screens/settings/components/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";

View File

@@ -55,6 +55,7 @@ import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view
import { strings } from "@notesnook/intl";
import { editorController } from "../../screens/editor/tiptap/utils";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
const getUser = () => {
const user = MMKV.getString("user");
@@ -96,9 +97,6 @@ const AppLocked = () => {
const lastAppState = useRef<AppStateStatus>(appState);
const biometricUnlockAwaitingUserInput = useRef(false);
const { height } = useWindowDimensions();
const keyboardType = useSettingStore(
(state) => state.settings.applockKeyboardType
);
const appLockHasPasswordSecurity = useSettingStore(
(state) => state.settings.appLockHasPasswordSecurity
);
@@ -157,8 +155,7 @@ const AppLocked = () => {
if (!appLockHasPasswordSecurity) {
await setAppLockVerificationCipher(password.current);
SettingsService.set({
appLockHasPasswordSecurity: true,
applockKeyboardType: "default"
appLockHasPasswordSecurity: true
});
DatabaseLogger.info("App lock migrated to password security");
}
@@ -167,7 +164,7 @@ const AppLocked = () => {
password.current = undefined;
} else {
ToastManager.show({
heading: strings.invalid(keyboardType),
heading: strings.invalid("password"),
type: "error",
context: "local"
});
@@ -290,9 +287,7 @@ const AppLocked = () => {
<Input
fwdRef={passwordInputRef}
secureTextEntry
keyboardType={
appLockHasPasswordSecurity ? keyboardType : "default"
}
keyboardType={"default"}
onLayout={async () => {
if (
!biometricsAuthEnabled ||
@@ -303,13 +298,7 @@ const AppLocked = () => {
}, 32);
}
}}
placeholder={
appLockHasPasswordSecurity
? keyboardType === "numeric"
? strings.enterApplockPassword()
: strings.enterApplockPin()
: strings.enterAccountPassword()
}
placeholder={PASSWORD_PLACEHOLDER}
onChangeText={(v) => (password.current = v)}
onSubmit={() => {
onSubmit();

View File

@@ -34,7 +34,7 @@ import create from "zustand";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import { AttachmentGroupProgress } from "../../screens/settings/attachment-group-progress";
import { AttachmentGroupProgress } from "../../screens/settings/components/attachment-group-progress";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { AppFontSize } from "../../utils/size";

View File

@@ -23,10 +23,9 @@ import React, { useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import BackupService from "../../services/backup";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { 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";
@@ -36,6 +35,8 @@ import FormInput, { createFormRef, validators } from "../ui/input/form-input";
import { Notice } from "../ui/notice";
import Paragraph from "../ui/typography/paragraph";
import { TextInput } from "react-native-gesture-handler";
import { Spacing } from "../../common/design/spacing";
import RecoveryKeySheet from "../sheets/recovery-key";
export const ChangePassword = () => {
const { colors } = useThemeColors();
@@ -92,7 +93,7 @@ export const ChangePassword = () => {
});
setLoading(false);
Navigation.goBack();
eSendEvent(eOpenRecoveryKeyDialog);
RecoveryKeySheet.present();
} catch (e) {
const message = (e as Error).message;
setLoading(false);
@@ -109,12 +110,15 @@ export const ChangePassword = () => {
<View
style={{
width: "100%",
padding: DefaultAppStyles.GAP
paddingTop: Spacing.LEVEL_0,
paddingHorizontal: Spacing.LEVEL_3,
gap: Spacing.LEVEL_2
}}
>
<Dialog context="change-password-dialog" />
<FormInput
name="oldPassword"
label={strings.oldPassword()}
formRef={formRef}
fwdRef={oldPasswordInputRef}
loading={loading}
@@ -125,7 +129,7 @@ export const ChangePassword = () => {
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.currentPassword()}
placeholder={"•••••••••"}
onSubmitEditing={() => {
passwordInputRef.current?.focus();
}}
@@ -133,6 +137,7 @@ export const ChangePassword = () => {
<FormInput
name="password"
label={strings.newPassword()}
formRef={formRef}
fwdRef={passwordInputRef}
loading={loading}
@@ -143,7 +148,28 @@ export const ChangePassword = () => {
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.newPassword()}
placeholder={"•••••••••"}
onSubmitEditing={() => {
changePassword();
}}
/>
<FormInput
name="confirmPassword"
label={strings.confirmPassword()}
formRef={formRef}
fwdRef={passwordInputRef}
loading={loading}
validators={[
validators.matchField("password", strings.confirmPasswordRequired())
]}
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={"•••••••••"}
onSubmitEditing={() => {
changePassword();
}}
@@ -168,15 +194,14 @@ export const ChangePassword = () => {
</Paragraph>
) : null}
<Notice text={strings.changePasswordNotice()} type="alert" />
<View style={{ height: 10 }} />
<Notice text={strings.changePasswordNotice2()} type="alert" />
<Notice
text={strings.changePasswordNotice()}
type="information"
size="small"
/>
<Button
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
loading={loading}
@@ -184,6 +209,23 @@ export const ChangePassword = () => {
type="accent"
title={loading ? null : strings.changePasswordConfirm()}
/>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1,
alignSelf: "center",
marginTop: Spacing.LEVEL_0
}}
>
<AppIcon
size={15}
name="shield-check"
iconFamily="notesnook"
color={colors.secondary.icon}
/>
<Paragraph>{strings.yourSecurityIsPriority()}</Paragraph>
</View>
</View>
);
};

View File

@@ -31,8 +31,7 @@ import ImagePreview from "../image-preview";
import MergeConflicts from "../merge-conflicts";
import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import Progress from "../dialogs/progress";
import Progress from "../dialogs/progress/progress";
import { useSettingStore } from "../../stores/use-setting-store";
const DialogProvider = () => {
@@ -51,7 +50,6 @@ const DialogProvider = () => {
{isAppLoading ? null : (
<>
<MergeConflicts />
<RecoveryKeySheet colors={colors} />
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />

View File

@@ -51,7 +51,7 @@ const DialogButtons = ({
{
flexDirection: "row",
gap: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_4
paddingHorizontal: Spacing.LEVEL_3
}
]}
>
@@ -61,7 +61,8 @@ const DialogButtons = ({
testID={notesnook.ids.default.dialog.no}
type="plain-outline"
style={{
width: "48.5%"
flexGrow: 1,
flexShrink: 1
}}
title={negativeTitle}
/>
@@ -71,7 +72,8 @@ const DialogButtons = ({
fontSize={AppFontSize.md}
testID={notesnook.ids.default.dialog.yes}
style={{
width: "48.5%"
flexGrow: 1,
flexShrink: 1
}}
loading={loading}
bold

View File

@@ -47,7 +47,8 @@ const DialogContainer = ({
maxHeight: height || 450,
borderRadius: Radius.LG,
backgroundColor: colors.primary.background,
paddingTop: 12
paddingTop: 12,
marginTop: -300
},
restProps?.noBorder
? {}

View File

@@ -17,17 +17,20 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Text, View, ViewStyle } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { Radius, Spacing } from "../../common/design/spacing";
import { DefaultAppStyles } from "../../utils/styles";
import AppIcon, { IconProps } from "../ui/AppIcon";
import { Button, ButtonProps } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
type DialogHeaderProps = {
icon?: string;
iconFamily?: IconProps["iconFamily"];
iconType?: "error" | "normal";
title?: string;
paragraph?: string;
button?: ButtonProps;
@@ -46,7 +49,10 @@ const DialogHeader = ({
padding,
centered,
titlePart,
style
style,
icon,
iconFamily,
iconType
}: DialogHeaderProps) => {
const { colors } = useThemeColors();
@@ -63,54 +69,83 @@ const DialogHeader = ({
>
<View
style={{
width: "100%"
width: "100%",
gap: Spacing.LEVEL_4
}}
>
{icon ? (
<View
style={{
alignSelf: centered ? "center" : "flex-start",
width: 40,
height: 40,
backgroundColor: colors.error.shade,
justifyContent: "center",
alignItems: "center",
borderRadius: Radius.XS
}}
>
<AppIcon
name={icon}
iconFamily={iconFamily}
color={
iconType == "error" ? colors.static.red : colors.primary.icon
}
/>
</View>
) : null}
<View
style={{
flexDirection: "row",
justifyContent: centered ? "center" : "space-between",
alignItems: "center"
gap: Spacing.LEVEL_1
}}
>
<Heading
style={{ textAlign: centered ? "center" : "left" }}
fontSize="XL"
<View
style={{
flexDirection: "row",
justifyContent: centered ? "center" : "space-between",
alignItems: "center"
}}
>
{title}{" "}
{titlePart ? (
<Text style={{ color: colors.primary.accent }}>
{titlePart}
</Text>
) : null}
</Heading>
<Heading
style={{ textAlign: centered ? "center" : "left" }}
fontSize="XL"
>
{title}{" "}
{titlePart ? (
<Text style={{ color: colors.primary.accent }}>
{titlePart}
</Text>
) : null}
</Heading>
{button ? (
<Button
{button ? (
<Button
style={{
borderRadius: 100,
paddingHorizontal: DefaultAppStyles.GAP
}}
fontSize={13}
type={button.type || "secondary"}
height={30}
{...button}
/>
) : null}
</View>
{paragraph ? (
<Paragraph
style={{
borderRadius: 100,
paddingHorizontal: DefaultAppStyles.GAP
textAlign: centered ? "center" : "left",
maxWidth: centered ? "90%" : "100%",
alignSelf: centered ? "center" : "flex-start"
}}
fontSize={13}
type={button.type || "secondary"}
height={30}
{...button}
/>
color={paragraphColor || colors.secondary.paragraph}
>
{paragraph}
</Paragraph>
) : null}
</View>
{paragraph ? (
<Paragraph
style={{
textAlign: centered ? "center" : "left",
maxWidth: centered ? "90%" : "100%",
alignSelf: centered ? "center" : "flex-start"
}}
color={paragraphColor || colors.secondary.paragraph}
>
{paragraph}
</Paragraph>
) : null}
</View>
</View>
</>

View File

@@ -23,10 +23,12 @@ import { eCloseSimpleDialog, eOpenSimpleDialog } from "../../utils/events";
import { ButtonProps } from "../ui/button";
import { FieldValidator, FormRef } from "../ui/input/form-input";
import { RefObject } from "react";
import { IconProps } from "../ui/AppIcon";
export type DialogInfo = {
title?: string;
paragraph?: string;
centered?: boolean;
positiveText: string;
negativeText: string;
background?: string;
@@ -44,6 +46,8 @@ export type DialogInfo = {
| "error"
| "errorShade";
icon?: string;
iconFamily?: IconProps["iconFamily"];
iconType?: "error" | "normal";
paragraphColor: string;
form?: {
formRef: FormRef;

View File

@@ -143,9 +143,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
}, [hide, show]);
const onNegativePress = async () => {
if (dialogInfo?.onClose) {
await dialogInfo.onClose();
}
hide();
};
@@ -218,6 +215,9 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
<DialogHeader
title={dialogInfo.title}
icon={dialogInfo.icon}
iconFamily={dialogInfo.iconFamily}
iconType={dialogInfo.iconType}
centered={dialogInfo.centered}
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
style={{
@@ -236,6 +236,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
key={item.name}
fwdRef={item.ref}
name={item.name}
label={item.label}
autoFocus={index === 0}
placeholder={item.placeholder}
formRef={formRef as RefObject<FormRef>}

View File

@@ -16,9 +16,11 @@ 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, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import {
clearAppLockVerificationCipher,
@@ -33,7 +35,7 @@ import {
eSubscribeEvent
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { PASSWORD_PLACEHOLDER } from "../../../utils/constants";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseAppLocKPasswordDailog,
@@ -42,61 +44,122 @@ import {
import { AppFontSize } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
import Heading from "../../ui/typography/heading";
export const AppLockPassword = () => {
const { colors } = useThemeColors();
const [mode, setMode] = useState<"create" | "change" | "remove">("create");
const [keyboardType, setKeyboardType] = useState<"pin" | "password">(
useSettingStore.getState().settings.applockKeyboardType === "default"
? "password"
: "pin"
);
const [visible, setVisible] = useState(false);
const currentPasswordInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const confirmPasswordInputRef = useRef<TextInput>(null);
const values = useRef<{
currentPassword?: string;
password?: string;
confirmPassword?: string;
}>({});
const [secureTextEntry, setSecureTextEntry] = useState(true);
const formRef = useRef(
createFormRef({
currentPassword: "",
password: "",
confirmPassword: ""
})
);
const [accountPass, setAccountPass] = useState(false);
const enableApplock = useRef(false);
useEffect(() => {
const subs = [
eSubscribeEvent(
eOpenAppLockPasswordDialog,
(mode: "create" | "change" | "remove") => {
(mode: "create" | "change" | "remove", _enableApplock = false) => {
setMode(mode);
setAccountPass(false);
setVisible(true);
enableApplock.current = _enableApplock;
}
),
eSubscribeEvent(eCloseAppLocKPasswordDailog, () => {
values.current = {};
setVisible(false);
close();
})
];
return () => {
subs.forEach((sub) => sub?.unsubscribe());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const close = () => {
values.current = {};
formRef.current.setValue("currentPassword", "");
formRef.current.setValue("password", "");
formRef.current.setValue("confirmPassword", "");
formRef.current.clearErrors();
setVisible(false);
};
const onSubmit = async () => {
if (!formRef.current.validate()) return;
const currentPassword = formRef.current.getValue("currentPassword");
const password = formRef.current.getValue("password");
if (mode === "create") {
setAppLockVerificationCipher(password);
SettingsService.setProperty("appLockHasPasswordSecurity", true);
if (enableApplock.current) {
SettingsService.setProperty("appLockEnabled", true);
}
} else if (mode === "change") {
const isCurrentPasswordCorrect =
await validateAppLockPassword(currentPassword);
if (!isCurrentPasswordCorrect) {
formRef.current.setError(
"currentPassword",
strings.incorrect("password")
);
return;
}
await clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", true);
await setAppLockVerificationCipher(password);
} else if (mode === "remove") {
const isCurrentPasswordCorrect = accountPass
? await db.user.verifyPassword(password)
: await validateAppLockPassword(password);
if (!isCurrentPasswordCorrect) {
formRef.current.setError(
"password",
accountPass
? strings.passwordIncorrect()
: strings.incorrect("password")
);
return;
}
clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometricService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
ToastManager.show({
message: strings.applockDisabled(),
type: "success"
});
}
}
close();
};
return !visible ? null : (
<BaseDialog
onShow={async () => {
@@ -115,286 +178,132 @@ export const AppLockPassword = () => {
style={{
...getElevationStyle(10),
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
borderRadius: Radius.MD,
backgroundColor: colors.primary.background,
paddingTop: 12
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_4,
gap: Spacing.LEVEL_4
}}
>
<DialogHeader
title={strings.changeAppLockCredentials(mode, keyboardType)}
icon="shield"
padding={12}
/>
<Seperator half />
<Heading fontSize="XL" lineHeight="100%">
{strings.changeAppLockCredentials(mode, "password")}
</Heading>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View style={{ gap: Spacing.LEVEL_2 }}>
{mode === "change" ? (
<Input
<FormInput
name="currentPassword"
formRef={formRef}
label={strings.currentPassword()}
fwdRef={currentPasswordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.currentPassword = value;
}}
onSubmit={() => {
passwordInputRef.current?.focus();
}}
defaultValue={values.current.currentPassword}
autoComplete="password"
onSubmitEditing={() => passwordInputRef.current?.focus()}
returnKeyLabel={strings.next()}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
returnKeyType="next"
secureTextEntry={secureTextEntry}
placeholder={
keyboardType === "pin"
? strings.currentPin()
: strings.currentPassword()
}
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
containerStyle={{ borderRadius: Radius.XS }}
validators={[validators.required(strings.passwordRequired())]}
/>
) : null}
<Input
<FormInput
name="password"
formRef={formRef}
label={
mode === "change"
? strings.newPassword()
: strings.enterPassword()
}
fwdRef={passwordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.password = value;
}}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.password}
keyboardType={
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
}
autoComplete="password"
onSubmitEditing={() => {
if (mode !== "remove") {
confirmPasswordInputRef.current?.focus();
} else {
onSubmit();
}
}}
returnKeyLabel={
mode !== "remove" ? strings.next() : strings.remove()
}
returnKeyType={mode !== "remove" ? "next" : "done"}
secureTextEntry={secureTextEntry}
buttonLeft={
accountPass ? null : (
<IconButton
name={keyboardType === "password" ? "numeric" : "keyboard"}
onPress={() => {
setKeyboardType(
keyboardType === "password" ? "pin" : "password"
);
setSecureTextEntry(false);
setImmediate(() => {
setSecureTextEntry(true);
});
}}
style={{
width: 25,
height: 25,
marginRight: 5
}}
size={AppFontSize.lg}
/>
)
}
placeholder={
accountPass
? strings.enterAccountPassword()
: mode === "change"
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin"
? strings.pin()
: strings.password()
}`
}
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
containerStyle={{ borderRadius: Radius.XS }}
validators={[validators.required(strings.passwordRequired())]}
/>
{mode !== "remove" ? (
<Input
fwdRef={confirmPasswordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.confirmPassword = value;
}}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.confirmPassword}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
customValidator={() => values.current.password || ""}
validationType="confirmPassword"
autoComplete="password"
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry={secureTextEntry}
placeholder={
keyboardType === "pin"
? strings.confirmPin()
<FormInput
name="confirmPassword"
formRef={formRef}
label={
mode === "change"
? strings.confirmNewPassword()
: strings.confirmPassword()
}
fwdRef={confirmPasswordInputRef}
autoCapitalize="none"
autoComplete="password"
onSubmitEditing={() => onSubmit()}
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
containerStyle={{ borderRadius: Radius.XS }}
validators={[
validators.required(strings.confirmPasswordRequired()),
validators.matchField("password", strings.passwordNotMatched())
]}
/>
) : null}
{mode === "remove" ? (
<>
<Button
icon={
accountPass ? "checkbox-marked" : "checkbox-blank-outline"
}
onPress={() => {
setSecureTextEntry(false);
setAccountPass(!accountPass);
setTimeout(() => {
setSecureTextEntry(true);
});
}}
iconSize={AppFontSize.lg}
type="plain"
iconColor={
accountPass ? colors.primary.accent : colors.primary.icon
}
title={strings.useAccountPassword()}
style={{
width: "100%",
alignSelf: "flex-start",
justifyContent: "flex-start",
paddingHorizontal: 0,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
/>
</>
<Button
icon={accountPass ? "checkbox" : "box-empty"}
iconFamily="notesnook"
onPress={() => setAccountPass(!accountPass)}
iconSize={AppFontSize.sm}
fontFamily="MEDIUM"
fontSize={AppFontSize.xs}
type="plain"
iconColor={
accountPass
? [colors.primary.accent, colors.primary.accentForeground]
: colors.primary.icon
}
textStyle={{
color: colors.primary.paragraph
}}
title={strings.useAccountPassword()}
style={{
width: "100%",
alignSelf: "flex-start",
justifyContent: "flex-start",
paddingHorizontal: 0,
paddingVertical: 0
}}
/>
) : null}
</View>
<DialogButtons
onPressNegative={close}
onPressPositive={async () => {
if (mode === "create") {
if (!values.current.password || !values.current.confirmPassword) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
return;
}
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(strings.mismatch(keyboardType)),
undefined,
"local"
);
return;
}
const password = values.current.password;
setAppLockVerificationCipher(password);
SettingsService.setProperty("appLockHasPasswordSecurity", true);
} else if (mode === "change") {
if (
!values.current.currentPassword ||
!values.current.password ||
!values.current.confirmPassword
) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
return;
}
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(strings.mismatch(keyboardType)),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.currentPassword
);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(strings.incorrect(keyboardType)),
undefined,
"local"
);
return;
}
const password = values.current.password;
await clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", true);
await setAppLockVerificationCipher(password);
} else if (mode === "remove") {
if (!values.current.password) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = accountPass
? await db.user.verifyPassword(values.current.password)
: await validateAppLockPassword(values.current.password);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
accountPass
? strings.passwordIncorrect()
: strings.incorrect(keyboardType)
),
undefined,
"local"
);
return;
}
clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometricService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
ToastManager.show({
message: strings.applockDisabled(),
type: "success"
});
}
}
SettingsService.setProperty(
"applockKeyboardType",
keyboardType === "password" ? "default" : "numeric"
);
close();
}}
positiveTitle={
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
}
negativeTitle={strings.cancel()}
positiveType="transparent"
loading={false}
doneText=""
/>
<View style={{ flexDirection: "row", gap: Spacing.LEVEL_2 }}>
<Button
title={strings.cancel()}
type="plain-outline"
onPress={close}
style={{ flex: 1, paddingVertical: Spacing.LEVEL_3 }}
/>
<Button
title={mode === "remove" ? strings.remove() : strings.save()}
type="accent"
onPress={onSubmit}
style={{ flex: 1, paddingVertical: Spacing.LEVEL_3 }}
/>
</View>
</View>
<Toast context="local" />
@@ -402,6 +311,9 @@ export const AppLockPassword = () => {
);
};
AppLockPassword.present = (mode: "create" | "change" | "remove") => {
eSendEvent(eOpenAppLockPasswordDialog, mode);
AppLockPassword.present = (
mode: "create" | "change" | "remove",
enableAppLock?: boolean
) => {
eSendEvent(eOpenAppLockPasswordDialog, mode, enableAppLock);
};

View File

@@ -45,6 +45,7 @@ import { IconButton } from "../../ui/icon-button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import ReactNativeBlobUtil from "react-native-blob-util";
import { useSettingStore } from "../../../stores/use-setting-store";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
@@ -286,6 +287,9 @@ const PDFPreview = () => {
color={colors.static.white}
name="open-in-new"
onPress={() => {
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
FileViewer.open(pdfSource, {
showOpenWithDialog: true,
showAppsSuggestions: true

View File

@@ -17,28 +17,8 @@ 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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
import { eSendEvent, eSubscribeEvent } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import DialogContainer from "../../dialog/dialog-container";
import { Button } from "../../ui/button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
export type ProgressOptions = {
progress?: string;
cancelCallback?: () => void;
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
};
import { eSendEvent } from "../../../services/event-manager";
import { ProgressOptions } from "./progress";
export const PROGRESS_EVENTS = {
start: "startProgress",
@@ -46,146 +26,6 @@ export const PROGRESS_EVENTS = {
update: "updateProgress"
};
export default function Progress() {
const { colors } = useThemeColors();
const [progress, setProgress] = React.useState<string | undefined>();
const [visible, setVisible] = React.useState(false);
const cancelCallback = React.useRef<() => void>(undefined);
const [data, setData] = React.useState<{
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
}>();
useEffect(() => {
const events = [
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
setProgress(options.progress);
cancelCallback.current = options.cancelCallback;
setData({
title: options.title,
paragraph: options.paragraph,
fillBackground: options.fillBackground,
canHideProgress: options.canHideProgress
});
setVisible(true);
}),
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
setProgress(undefined);
setVisible(false);
setData(undefined);
cancelCallback.current?.();
cancelCallback.current = undefined;
}),
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
setProgress(options.progress);
if (options.cancelCallback) {
cancelCallback.current = options.cancelCallback;
}
const data: ProgressOptions = {};
if (options.title) data.title = options.title;
if (options.paragraph) data.paragraph = options.paragraph;
if (options.fillBackground)
data.fillBackground = options.fillBackground;
setData((current) => {
return {
...current,
...data
};
});
})
];
return () => {
events.forEach((event) => event?.unsubscribe());
cancelCallback.current = undefined;
};
}, []);
return !visible ? null : (
<BaseDialog
background={data?.fillBackground ? colors.primary.background : undefined}
visible
>
<DialogContainer
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingBottom: 10
}}
noBorder={data?.fillBackground ? true : false}
>
<Dialog context="local" />
<View
style={{
justifyContent: "center",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
gap: 10,
paddingBottom: 20
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.paragraph}
size={AppFontSize.lg}
>
{data?.title}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
>
{progress ? progress : data?.paragraph}
</Paragraph>
<View
style={{
flexDirection: "row",
width: 100,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<ProgressBarComponent
height={5}
width={100}
animated={true}
useNativeDriver
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
{!data?.canHideProgress ? null : (
<Button
title={cancelCallback.current ? "Cancel" : "Hide"}
type="secondaryAccented"
onPress={() => {
if (cancelCallback.current) {
cancelCallback.current?.();
}
setVisible(false);
setProgress(undefined);
setData(undefined);
}}
width="100%"
/>
)}
</View>
</DialogContainer>
</BaseDialog>
);
}
export function startProgress(options: ProgressOptions) {
eSendEvent(PROGRESS_EVENTS.start, options);
}

View File

@@ -0,0 +1,210 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
import { PROGRESS_EVENTS } from ".";
import { Radius, Spacing } from "../../../common/design/spacing";
import { eSubscribeEvent } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import DialogContainer from "../../dialog/dialog-container";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
export type ProgressOptions = {
progress?: string;
cancelCallback?: () => void;
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
icon?: string;
};
export default function Progress() {
const { colors } = useThemeColors();
const [progress, setProgress] = React.useState<string | undefined>();
const [visible, setVisible] = React.useState(false);
const cancelCallback = React.useRef<() => void>(undefined);
const [data, setData] = React.useState<{
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
icon?: string;
}>();
useEffect(() => {
const events = [
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
setProgress(options.progress);
cancelCallback.current = options.cancelCallback;
setData({
...options
});
setVisible(true);
}),
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
setProgress(undefined);
setVisible(false);
setData(undefined);
cancelCallback.current?.();
cancelCallback.current = undefined;
}),
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
setProgress(options.progress);
if (options.cancelCallback) {
cancelCallback.current = options.cancelCallback;
}
const data: ProgressOptions = {};
if (options.title) data.title = options.title;
if (options.paragraph) data.paragraph = options.paragraph;
if (options.fillBackground)
data.fillBackground = options.fillBackground;
if (options.icon) {
data.icon = options.icon;
}
setData((current) => {
return {
...current,
...data
};
});
})
];
return () => {
events.forEach((event) => event?.unsubscribe());
cancelCallback.current = undefined;
};
}, []);
return !visible ? null : (
<BaseDialog
background={data?.fillBackground ? colors.primary.background : undefined}
visible
>
<DialogContainer
style={{
padding: Spacing.LEVEL_4
}}
noBorder={data?.fillBackground ? true : false}
>
<Dialog context="local" />
<View
style={{
justifyContent: "center",
alignItems: "center",
gap: Spacing.LEVEL_4,
paddingTop: Spacing.LEVEL_1
}}
>
{data?.icon ? (
<View
style={{
width: 40,
height: 40,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name={data?.icon}
iconFamily="notesnook"
size={20}
color={colors.primary.icon}
/>
</View>
) : null}
<View
style={{
gap: Spacing.LEVEL_1
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.heading}
size={AppFontSize.lg}
>
{data?.title}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
>
{progress ? progress : data?.paragraph}
</Paragraph>
<View
style={{
flexDirection: "row",
width: "100%"
}}
>
<ProgressBarComponent
height={8}
width={null}
style={{
flex: 1
}}
animated={true}
useNativeDriver
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
</View>
{!data?.canHideProgress ? null : (
<Button
title={cancelCallback.current ? "Cancel" : "Hide"}
type="accent"
onPress={() => {
if (cancelCallback.current) {
cancelCallback.current?.();
}
setVisible(false);
setProgress(undefined);
setData(undefined);
}}
width={160}
/>
)}
</View>
</DialogContainer>
</BaseDialog>
);
}

View File

@@ -55,7 +55,6 @@ import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
@@ -67,9 +66,12 @@ import {
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../../stores/use-user-store";
import { AppFontSize } from "../../../utils/size";
import { Pressable } from "../../ui/pressable";
import AppIcon from "../../ui/AppIcon";
import { Radius, Spacing } from "../../../common/design/spacing";
import { PASSWORD_PLACEHOLDER } from "../../../utils/constants";
import { AppFontSize } from "../../../utils/size";
import { useSettingStore } from "../../../stores/use-setting-store";
export const VaultDialog: React.FC = () => {
const { colors } = useThemeColors();
@@ -91,7 +93,7 @@ export const VaultDialog: React.FC = () => {
const paragraphRef = useRef<string | null>(null);
const buttonTitleRef = useRef<string | null>(null);
const positiveButtonTypeRef = useRef<"errorShade" | "transparent" | "accent">(
"transparent"
"accent"
);
const customActionTitleRef = useRef<string | null>(null);
const customActionParagraphRef = useRef<string | null>(null);
@@ -119,6 +121,7 @@ export const VaultDialog: React.FC = () => {
const passInputRef = useRef<TextInput>(null);
const confirmPassRef = useRef<TextInput>(null);
const newPassInputRef = useRef<TextInput>(null);
const [icon, setIcon] = useState<string>();
const close = useCallback(() => {
if (loading) {
@@ -146,7 +149,7 @@ export const VaultDialog: React.FC = () => {
descriptionRef.current = null;
paragraphRef.current = null;
buttonTitleRef.current = null;
positiveButtonTypeRef.current = "transparent";
positiveButtonTypeRef.current = "accent";
customActionTitleRef.current = null;
customActionParagraphRef.current = null;
noteLockedRef.current = false;
@@ -355,6 +358,7 @@ export const VaultDialog: React.FC = () => {
async (note: Note & { content?: NoteContent<false> }) => {
close();
try {
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
await Share.open({
title: note.title,
failOnCancel: false,
@@ -638,6 +642,7 @@ export const VaultDialog: React.FC = () => {
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
setIcon(data.icon);
setDeleteAll(false);
setLoading(false);
@@ -707,9 +712,9 @@ export const VaultDialog: React.FC = () => {
style={{
...getElevationStyle(5),
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
borderRadius: Radius.MD,
backgroundColor: colors.primary.background,
paddingTop: 12,
paddingVertical: Spacing.LEVEL_4,
overflow: "hidden"
}}
>
@@ -718,13 +723,21 @@ export const VaultDialog: React.FC = () => {
paragraph={
paragraphRef.current || customActionParagraphRef.current || ""
}
icon="shield"
padding={12}
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
centered={!!icon}
icon={icon}
iconType="error"
iconFamily="notesnook"
/>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
paddingHorizontal: Spacing.LEVEL_3,
gap: isDeleteVault ? 0 : Spacing.LEVEL_4,
marginTop: Spacing.LEVEL_4,
marginBottom: Spacing.LEVEL_1
}}
>
{(isChangePassword ||
@@ -743,20 +756,16 @@ export const VaultDialog: React.FC = () => {
<FormInput
name="password"
formRef={formRef}
label={
isChangePassword
? strings.currentPassword()
: strings.password()
}
fwdRef={passInputRef}
editable={!loading}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
autoComplete="password"
marginBottom={
!biometricUnlock ||
!isBiometryEnrolled ||
isCreateVault ||
isChangePassword ||
isCustomAction
? 0
: 10
}
onSubmitEditing={() => {
if (isChangePassword) {
newPassInputRef.current?.focus();
@@ -769,11 +778,7 @@ export const VaultDialog: React.FC = () => {
}
returnKeyType={isChangePassword ? "next" : "done"}
secureTextEntry
placeholder={
isChangePassword
? strings.currentPassword()
: strings.password()
}
placeholder={PASSWORD_PLACEHOLDER}
validators={[validators.required(strings.passwordRequired())]}
/>
@@ -800,37 +805,30 @@ export const VaultDialog: React.FC = () => {
{isDeleteVault && (
<Pressable
onPress={() => setDeleteAll(!deleteAll)}
icon={deleteAll ? "checkbox" : "box-empty"}
iconFamily="notesnook"
fontFamily="MEDIUM"
iconSize={14}
fontSize={AppFontSize.sm}
width="100%"
style={{
paddingVertical: 0,
flexDirection: "row",
gap: DefaultAppStyles.GAP_SMALL,
marginTop: isUserLoggedIn
? DefaultAppStyles.GAP_VERTICAL_SMALL
: 0,
justifyContent: "flex-start"
justifyContent: "flex-start",
paddingHorizontal: 0
}}
>
<AppIcon
name={
deleteAll
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
color={colors.error.accent}
size={AppFontSize.md}
/>
<Paragraph color={colors.error.accent} size={AppFontSize.sm}>
{strings.deleteAllNotes()}
</Paragraph>
</Pressable>
title={strings.deleteAllNotes()}
type="transparent"
iconColor={colors.error.accent}
textStyle={{
color: colors.error.accent
}}
/>
)}
{isChangePassword ? (
<>
<Seperator half />
<FormInput
name="newPassword"
label={strings.newPassword()}
formRef={formRef}
fwdRef={newPassInputRef}
editable={!loading}
@@ -843,7 +841,7 @@ export const VaultDialog: React.FC = () => {
returnKeyLabel="Change"
returnKeyType="done"
secureTextEntry
placeholder={strings.newPassword()}
placeholder={PASSWORD_PLACEHOLDER}
validators={[validators.required(strings.passwordRequired())]}
/>
</>
@@ -864,12 +862,13 @@ export const VaultDialog: React.FC = () => {
onSubmitEditing={() => {
confirmPassRef.current?.focus();
}}
placeholder={strings.password()}
placeholder={PASSWORD_PLACEHOLDER}
validators={[validators.required(strings.passwordRequired())]}
/>
<FormInput
name="confirmPassword"
label={strings.confirmPassword()}
formRef={formRef}
fwdRef={confirmPassRef}
autoCapitalize="none"
@@ -882,7 +881,7 @@ export const VaultDialog: React.FC = () => {
onSubmitEditing={() => {
onPress();
}}
placeholder={strings.confirmPassword()}
placeholder={PASSWORD_PLACEHOLDER}
validators={[
validators.required(strings.confirmPasswordRequired()),
validators.matchField(

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { View } from "react-native";
import { View, ViewStyle } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import { Radius, Spacing } from "../../common/design/spacing";
import {
@@ -44,7 +44,8 @@ export const Header = ({
canGoBack,
hasSearch,
onSearch,
rightButton
rightButton,
style
}: {
onLeftMenuButtonPress?: () => void;
renderedInRoute?: RouteName;
@@ -55,6 +56,7 @@ export const Header = ({
hasSearch?: boolean;
onSearch?: () => void;
rightButton?: IconButtonProps;
style?: ViewStyle;
}) => {
const { colors } = useThemeColors();
const [borderHidden, setBorderHidden] = useState(true);
@@ -109,15 +111,18 @@ export const Header = ({
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
borderRadius: Radius.S,
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_3,
backgroundColor: colors.secondary.background,
alignItems: "center"
}}
style={[
{
flexDirection: "row",
justifyContent: "space-between",
borderRadius: Radius.S,
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_3,
backgroundColor: colors.secondary.background,
alignItems: "center"
},
style
]}
testID="search-header"
>
{isTablet && !canGoBack ? null : (
@@ -134,7 +139,7 @@ export const Header = ({
height: 20
}}
size={20}
name={canGoBack ? "arrow-left" : "menu"}
name={canGoBack ? "arrow-back" : "menu"}
iconFamily="notesnook"
color={colors.primary.icon}
/>

View File

@@ -34,6 +34,8 @@ import SheetWrapper from "../ui/sheet";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
import { Radius, Spacing } from "../../common/design/spacing";
import AppIcon from "../ui/AppIcon";
const SheetProvider = ({ context = "global" }) => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
@@ -113,43 +115,78 @@ const SheetProvider = ({ context = "global" }) => {
: data.enableGesturesInScrollView
}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginBottom:
!data.progress && !data.icon && !data.title && !data.paragraph
? 0
: 10,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{data?.progress ? (
<ActivityIndicator
{data?.progress || data?.icon || data?.paragraph ? (
<View
style={{
justifyContent: "center",
alignItems: "center",
marginBottom:
!data.progress && !data.icon && !data.title && !data.paragraph
? 0
: 10,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View
style={{
marginTop: DefaultAppStyles.GAP
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
paddingTop: Spacing.LEVEL_2
}}
size={50}
color={colors.primary.accent}
/>
) : null}
>
{data?.icon || data?.progress ? (
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
{data.icon ? (
<AppIcon
name="clock"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
) : (
<ActivityIndicator
style={{
marginTop: DefaultAppStyles.GAP
}}
size={50}
color={colors.primary.accent}
/>
)}
</View>
) : null}
{data?.icon ? (
<Icon
color={colors[data.iconColor] || colors.primary.accent}
name={data.icon}
size={50}
/>
) : null}
{data?.title ? <Heading> {data?.title}</Heading> : null}
{data?.paragraph ? (
<Paragraph style={{ textAlign: "center" }}>
{data?.paragraph}
</Paragraph>
) : null}
</View>
{data?.title || data?.paragraph ? (
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
{data.title ? (
<Heading fontSize="XL" lineHeight="100%">
{data.title}
</Heading>
) : null}
{data.paragraph ? (
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{data.paragraph}
</Paragraph>
) : null}
</View>
) : null}
</View>
</View>
) : null}
{typeof data.component === "function"
? data.component(
@@ -212,20 +249,27 @@ const SheetProvider = ({ context = "global" }) => {
/>
) : null}
{data?.actionsArray &&
data?.actionsArray.map((item) => (
<Button
onPress={item.action}
key={item.accentText}
title={item.actionText}
icon={item.icon && item.icon}
type={item.type || "accent"}
style={{
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
width="100%"
/>
))}
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2
}}
>
{data?.actionsArray &&
data?.actionsArray.map((item) => (
<Button
onPress={item.action}
key={item.accentText}
title={item.actionText}
icon={item.icon && item.icon}
type={item.type || "accent"}
style={{
flex: 1,
flexShrink: 1
}}
/>
))}
</View>
{data?.learnMore ? (
<Paragraph

View File

@@ -16,23 +16,24 @@ 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 { View } from "react-native";
import { strings } from "@notesnook/intl";
import { ScrollView } from "react-native-actions-sheet";
import { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import { ToastManager, presentSheet } from "../../../services/event-manager";
import { DefaultAppStyles } from "../../../utils/styles";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
import Paragraph from "../../ui/typography/paragraph";
import Heading from "../../ui/typography/heading";
import { DefaultAppStyles } from "../../../utils/styles";
import { AppFontSize } from "../../../utils/size";
import { ToastManager, presentSheet } from "../../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { Pressable } from "../../ui/pressable";
import { ScrollView } from "react-native-actions-sheet";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const getExpiryOptions = () => [
{ label: strings.expiryOneDay(), value: 24 * 60 * 60 * 1000 },
@@ -94,7 +95,7 @@ export default function AddApiKeySheet({ close, onAdd }: AddApiKeySheetProps) {
<ScrollView
contentContainerStyle={{
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL,
gap: DefaultAppStyles.GAP,
paddingTop: DefaultAppStyles.GAP_VERTICAL,
paddingBottom: DefaultAppStyles.GAP_VERTICAL * 2
}}
@@ -102,60 +103,92 @@ export default function AddApiKeySheet({ close, onAdd }: AddApiKeySheetProps) {
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
alignItems: "flex-start",
gap: DefaultAppStyles.GAP_SMALL
}}
>
<Heading size={AppFontSize.xl}>{strings.createApiKey()}</Heading>
</View>
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
<Paragraph size={AppFontSize.sm}>{strings.keyName()}</Paragraph>
<FormInput
name="keyName"
formRef={formRef}
placeholder={strings.exampleKeyName()}
validators={[validators.required(strings.enterKeyName())]}
onChangeText={() => {
formRef.current.setError("keyName", undefined);
}}
onSubmitEditing={handleCreate}
/>
</View>
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
<Paragraph size={AppFontSize.sm}>{strings.expiresIn()}</Paragraph>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: DefaultAppStyles.GAP_SMALL
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
{getExpiryOptions().map((option) => (
<Pressable
key={option.label}
onPress={() => setSelectedExpiry(option.value)}
type={
selectedExpiry === option.value ? "selected" : "transparent"
}
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph
size={AppFontSize.sm}
color={
selectedExpiry === option.value
? colors.selected.paragraph
: colors.primary.paragraph
}
<AppIcon
name="key"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View style={{ flex: 1, gap: DefaultAppStyles.GAP_VERTICAL_SMALL }}>
<Heading fontSize="XL" lineHeight="100%">
{strings.createApiKey()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.createApiKeyDesc()}
</Paragraph>
</View>
</View>
<FormInput
name="keyName"
formRef={formRef}
label={strings.keyName()}
placeholder={strings.exampleKeyName()}
validators={[validators.required(strings.enterKeyName())]}
containerStyle={{ borderRadius: Radius.XS }}
onChangeText={() => {
formRef.current.setError("keyName", undefined);
}}
onSubmitEditing={handleCreate}
/>
<View style={{ height: 1, backgroundColor: colors.primary.border }} />
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
<Heading fontSize="MD" lineHeight="100%">
{strings.expiresIn()}
</Heading>
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
{getExpiryOptions().map((option) => {
const selected = selectedExpiry === option.value;
return (
<Pressable
key={option.label}
onPress={() => setSelectedExpiry(option.value)}
type={selected ? "selected" : "transparent"}
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: selected ? 0 : 1,
borderColor: colors.secondary.border
}}
>
{option.label}
</Paragraph>
</Pressable>
))}
<Heading
fontFamily="MEDIUM"
fontSize="SM"
lineHeight="100%"
color={
selected ? colors.selected.heading : colors.secondary.heading
}
>
{option.label}
</Heading>
<AppIcon
name={selected ? "radiobox-marked" : "radiobox-blank"}
size={16}
color={selected ? colors.selected.accent : colors.secondary.icon}
/>
</Pressable>
);
})}
</View>
</View>
@@ -166,9 +199,6 @@ export default function AddApiKeySheet({ close, onAdd }: AddApiKeySheetProps) {
loading={isCreating}
disabled={isCreating}
onPress={handleCreate}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
/>
</ScrollView>
);

View File

@@ -0,0 +1,178 @@
/*
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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { useWindowDimensions, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { Radius, Spacing } from "../../../common/design/spacing";
import { presentSheet } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const OPTIONS = [-1, 0, 1, 5, 15, 30];
const formatValue = (item: number) => {
return item === -1
? strings.never()
: item === 0
? strings.immediately()
: item === 1
? strings.minutes(1)
: strings.minutes(item);
};
type AppLockTimeoutProps = {
close?: (ctx?: string) => void;
};
function AppLockTimeout({ close }: AppLockTimeoutProps) {
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const [currentValue, setCurrentValue] = useState(
useSettingStore.getState().settings.appLockTimer
);
const onChange = async (item: number) => {
SettingsService.set({ appLockTimer: item });
setCurrentValue(item);
close?.();
};
return (
<ScrollView
style={{
maxHeight: height * 0.8
}}
>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
paddingTop: Spacing.LEVEL_2
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="clock"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{strings.appLockTimeout()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.appLockTimeoutDesc()}
</Paragraph>
</View>
</View>
</View>
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_2
}}
>
{OPTIONS.map((item) => {
const selected = currentValue === item;
return (
<Pressable
key={item}
type={selected ? "selected" : "transparent"}
onPress={() => onChange(item)}
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: selected ? 0 : 1,
borderColor: colors.primary.border
}}
>
<Heading
fontFamily="MEDIUM"
fontSize="SM"
lineHeight="100%"
color={colors.primary.heading}
>
{formatValue(item)}
</Heading>
<AppIcon
name={selected ? "radio-button" : "ellipse"}
iconFamily="notesnook"
size={16}
color={
selected
? [colors.selected.accent, colors.static.white]
: colors.secondary.icon
}
/>
</Pressable>
);
})}
</View>
</ScrollView>
);
}
AppLockTimeout.present = () => {
presentSheet({
component: (_ref, close) => <AppLockTimeout close={close} />
});
};
export default AppLockTimeout;

View File

@@ -0,0 +1,185 @@
/*
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 { DATE_FORMATS } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React, { useState } from "react";
import { useWindowDimensions, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { Radius, Spacing } from "../../../common/design/spacing";
import { presentSheet } from "../../../services/event-manager";
import { useSettingStore } from "../../../stores/use-setting-store";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
type DateFormatProps = {
close?: (ctx?: string) => void;
};
function DateFormat({ close }: DateFormatProps) {
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const [currentValue, setCurrentValue] = useState(db.settings.getDateFormat());
const onChange = async (item: string) => {
db.settings.setDateFormat(item);
useSettingStore.setState({
dateFormat: item
});
setCurrentValue(item);
close?.();
};
return (
<ScrollView
style={{
maxHeight: height * 0.8
}}
>
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="calendar-day"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{strings.dateFormat()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.dateFormatDesc()}
</Paragraph>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: colors.primary.border
}}
/>
<View
style={{
gap: Spacing.LEVEL_2
}}
>
{DATE_FORMATS.map((item) => {
const selected = currentValue === item;
return (
<Pressable
key={item}
type={selected ? "selected" : "transparent"}
onPress={() => onChange(item)}
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: selected ? 0 : 1,
borderColor: colors.primary.border
}}
>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading
fontFamily="MEDIUM"
fontSize="SM"
lineHeight="100%"
color={colors.primary.heading}
>
{dayjs().format(item)}
</Heading>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{item}
</Paragraph>
</View>
<AppIcon
name={selected ? "radio-button" : "ellipse"}
iconFamily="notesnook"
size={16}
color={
selected
? [colors.selected.accent, colors.static.white]
: colors.secondary.icon
}
/>
</Pressable>
);
})}
</View>
</View>
</ScrollView>
);
}
DateFormat.present = () => {
presentSheet({
component: (_ref, close) => <DateFormat close={close} />
});
};
export default DateFormat;

View File

@@ -0,0 +1,378 @@
/*
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 { format, logManager } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Linking, Platform, View } from "react-native";
import FileViewer from "react-native-file-viewer";
import ReactNativeBlobUtil from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import { zip } from "react-native-zip-archive";
import filesystem from "../../../common/filesystem";
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
import { Radius, Spacing } from "../../../common/design/spacing";
import { presentSheet } from "../../../services/event-manager";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { sleep } from "../../../utils/time";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { DatabaseLogger } from "../../../common/database";
type DownloadLogsProps = {
close?: (ctx?: string) => void;
};
type DownloadResult = {
/** Local zip file path used for sharing/opening the archive. */
localPath: string;
/** Directory the archive was saved into (for "open location"). */
directory: string;
};
type Status = "loading" | "success" | "error";
const IconTile = ({
name,
color,
backgroundColor
}: {
name: string;
color: string;
backgroundColor: string;
}) => (
<View
style={{
width: 40,
height: 40,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor
}}
>
<AppIcon name={name} iconFamily="notesnook" size={16} color={color} />
</View>
);
async function downloadLogs(
onStep: (message: string, progress: number) => void
): Promise<DownloadResult> {
onStep(strings.preparingLogsChooseLocation(), 0.1);
let directory =
Platform.OS === "ios"
? await filesystem.checkAndCreateDir(`/debug-logs/`)
: undefined;
if (Platform.OS === "android") {
useUserStore.setState({ disableAppLockRequests: true });
const file = await ScopedStorage.openDocumentTree(true);
setTimeout(() => {
useUserStore.setState({ disableAppLockRequests: false });
}, 1000);
if (!file) throw new Error("no-directory");
directory = file.uri;
}
if (!directory) throw new Error("no-directory");
onStep(strings.preparingLogsCollect(), 0.35);
const logs = await logManager?.get();
if (!logs) throw new Error("no-logs");
const logsDir = cacheDir + `/notesnook-debug-logs`;
if (await ReactNativeBlobUtil.fs.exists(logsDir)) {
await ReactNativeBlobUtil.fs.unlink(logsDir);
}
await ReactNativeBlobUtil.fs.mkdir(logsDir);
for (const logGroup of logs) {
let logString = ``;
for (const log of logGroup.logs) {
logString += `\n${format(log)}`;
}
await ReactNativeBlobUtil.fs.createFile(
`${logsDir}/${logGroup.key}.txt`,
logString,
"utf8"
);
}
onStep(strings.preparingLogsCompress(), 0.7);
const fileName = `notesnook-debug-logs-${Date.now()}.zip`;
const outputPath = await zip(
logsDir,
Platform.OS === "android" ? logsDir + ".zip" : `${directory}/${fileName}`
);
onStep(strings.preparingLogsSave(), 0.9);
if (Platform.OS === "android") {
const file = await ScopedStorage.createFile(
directory,
fileName,
"application/zip"
);
await copyFileAsync("file://" + outputPath, file.uri);
// keep `outputPath` (the cache copy) around so it can be shared/opened.
return { localPath: outputPath, directory };
}
await ReactNativeBlobUtil.fs.unlink(logsDir);
await sleep(1000);
return { localPath: outputPath, directory };
}
function DownloadLogs({ close }: DownloadLogsProps) {
const { colors } = useThemeColors();
const [status, setStatus] = useState<Status>("loading");
const [step, setStep] = useState({
message: strings.preparingLogsChooseLocation(),
progress: 0.1
});
const result = useRef<DownloadResult | undefined>(undefined);
const started = useRef(false);
const start = useCallback(async () => {
setStatus("loading");
setStep({
message: strings.preparingLogsChooseLocation(),
progress: 0.1
});
try {
result.current = await downloadLogs((message, progress) =>
setStep({ message, progress })
);
setStatus("success");
} catch (e) {
DatabaseLogger.error(e as Error);
result.current = undefined;
setStatus("error");
}
}, []);
useEffect(() => {
if (started.current) return;
started.current = true;
start();
}, [start]);
return (
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
paddingBottom: Spacing.LEVEL_4,
gap: Spacing.LEVEL_3
}}
>
{status === "loading" ? (
<View
style={{
alignItems: "center",
justifyContent: "center",
gap: Spacing.LEVEL_2,
width: "100%"
}}
>
<IconTile
name="clock"
color={colors.primary.icon}
backgroundColor={colors.secondary.background}
/>
<Heading fontSize="XL" lineHeight="100%">
{strings.preparingLogs()}
</Heading>
<Paragraph
fontSize="SM"
color={colors.secondary.paragraph}
style={{ textAlign: "center" }}
>
{step.message}
</Paragraph>
<View
style={{
width: "100%",
height: 8,
borderRadius: Radius.XXL,
backgroundColor: colors.secondary.background,
overflow: "hidden"
}}
>
<View
style={{
width: `${Math.round(step.progress * 100)}%`,
height: 8,
borderRadius: Radius.XXL,
backgroundColor: colors.primary.accent
}}
/>
</View>
</View>
) : status === "error" ? (
<View style={{ width: "100%", gap: Spacing.LEVEL_3 }}>
<View style={{ alignItems: "center", gap: Spacing.LEVEL_2 }}>
<IconTile
name="warning"
color={colors.error.accent}
backgroundColor={colors.error.background}
/>
<View style={{ gap: Spacing.LEVEL_1, width: "100%" }}>
<Heading
fontSize="XL"
lineHeight="120%"
style={{ textAlign: "center" }}
>
{strings.downloadLogsFailed()}
</Heading>
<Paragraph
fontSize="SM"
color={colors.secondary.paragraph}
style={{ textAlign: "center" }}
>
{strings.downloadLogsFailedDesc()}
</Paragraph>
</View>
</View>
<Button
title={strings.tryDownloadLogsAgain()}
type="accent"
width="100%"
style={{ borderRadius: Radius.S }}
onPress={start}
/>
</View>
) : (
<View style={{ width: "100%", gap: Spacing.LEVEL_3 }}>
<View style={{ alignItems: "center", gap: Spacing.LEVEL_2 }}>
<IconTile
name="checks"
color={colors.primary.accent}
backgroundColor={colors.secondary.background}
/>
<View style={{ gap: Spacing.LEVEL_1, width: "100%" }}>
<Heading
fontSize="XL"
lineHeight="120%"
style={{ textAlign: "center" }}
>
{strings.debugLogsDownloaded()}
</Heading>
<Paragraph
fontSize="SM"
color={colors.secondary.paragraph}
style={{ textAlign: "center" }}
>
{strings.debugLogsDownloadedDesc()}
</Paragraph>
</View>
</View>
<View style={{ width: "100%", gap: Spacing.LEVEL_2 }}>
<View style={{ flexDirection: "row", gap: Spacing.LEVEL_2 }}>
<Button
title={strings.shareZip()}
type="plain-outline"
style={{ flex: 1, borderRadius: Radius.S }}
onPress={async () => {
const path = result.current?.localPath;
if (!path) return;
close?.();
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "ios") {
await sleep(500);
Share.open({ url: path }).catch(() => {
/* empty */
});
} else {
FileViewer.open(path, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
} as any).catch(() => {
/* empty */
});
}
}}
/>
<Button
title={strings.openFileLocation()}
type="plain-outline"
style={{ flex: 1, borderRadius: Radius.S }}
onPress={async () => {
const path = result.current?.localPath;
const directory = result.current?.directory;
if (!path) return;
close?.();
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "android") {
if (directory) {
Linking.openURL(directory).catch(() => {
/* empty */
});
}
} else {
await sleep(500);
FileViewer.open(path, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch(() => {
/* empty */
});
}
}}
/>
</View>
<Button
title={strings.goBack()}
type="accent"
width="100%"
style={{ borderRadius: Radius.S }}
onPress={() => close?.()}
/>
</View>
</View>
)}
</View>
);
}
DownloadLogs.present = () => {
presentSheet({
component: (_ref, close) => <DownloadLogs close={close} />
});
};
export default DownloadLogs;

View File

@@ -19,18 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { Fragment, useState } from "react";
import {
ActivityIndicator,
Linking,
Platform,
StyleSheet,
View
} from "react-native";
import React, { useState } from "react";
import { ActivityIndicator, Linking, Platform, View } from "react-native";
import FileViewer from "react-native-file-viewer";
import Share from "react-native-share";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import { requestInAppReview } from "../../../services/app-review";
import {
@@ -40,16 +34,11 @@ import {
} from "../../../services/event-manager";
import Exporter from "../../../services/exporter";
import { useSettingStore } from "../../../stores/use-setting-store";
import { getElevationStyle } from "../../../utils/elevation";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { sleep } from "../../../utils/time";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
@@ -117,7 +106,7 @@ const ExportNotesSheet = ({
func: async () => {
await exportNoteAs("pdf");
},
icon: "file-pdf-box",
icon: "file-pdf",
id: notesnook.ids.dialogs.export.pdf
},
{
@@ -125,7 +114,7 @@ const ExportNotesSheet = ({
func: async () => {
await exportNoteAs("md");
},
icon: "language-markdown",
icon: "markdown",
id: notesnook.ids.dialogs.export.md
},
{
@@ -133,7 +122,7 @@ const ExportNotesSheet = ({
func: async () => {
await exportNoteAs("md-frontmatter");
},
icon: "language-markdown",
icon: "markdown",
id: notesnook.ids.dialogs.export.md
},
{
@@ -141,7 +130,7 @@ const ExportNotesSheet = ({
func: async () => {
await exportNoteAs("txt");
},
icon: "card-text",
icon: "file-text",
id: notesnook.ids.dialogs.export.text
},
{
@@ -149,215 +138,222 @@ const ExportNotesSheet = ({
func: async () => {
await exportNoteAs("html");
},
icon: "language-html5",
icon: "file-html",
id: notesnook.ids.dialogs.export.html
}
];
return (
<View>
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3
}}
>
<Dialog context="export-notes" />
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="export"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View
style={{
flex: 1,
justifyContent: "center",
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{complete
? strings.exportSuccessHeading(ids.length)
: strings.exportNotes(ids.length)}
</Heading>
<Paragraph>
{complete
? strings.exportSuccessDesc(result?.fileName as string)
: strings.exportAllNotesDesc()}
</Paragraph>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: colors.primary.border
}}
/>
{!complete && !exporting ? (
<>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
gap: Spacing.LEVEL_0
}}
>
<DialogHeader
icon="export"
title={strings.exportNotes(ids.length)}
/>
</View>
<Seperator half />
</>
) : null}
<Dialog context="export-notes" />
<View style={styles.buttonContainer}>
{!exporting && !complete ? (
actions.map((item) => (
<Fragment key={item.title}>
<Seperator half />
{actions.map((item) => (
<Pressable
key={item.title}
testID={item.id}
type="transparent"
onPress={item.func}
style={{
width: "100%",
alignItems: "center",
flexDirection: "row",
paddingRight: 12,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
alignItems: "center",
justifyContent: "flex-start",
borderRadius: 0,
paddingHorizontal: DefaultAppStyles.GAP
gap: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_1,
borderRadius: Radius.S
}}
>
<View
style={{
backgroundColor: colors.primary.shade,
borderRadius: defaultBorderRadius,
height: 60,
width: 60,
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
alignItems: "center"
backgroundColor: colors.secondary.background
}}
>
<Icon
<AppIcon
name={item.icon}
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
size={AppFontSize.xxxl + 10}
/>
</View>
<View
style={{
flexShrink: 1
}}
>
<Heading style={{ marginLeft: 10 }} size={AppFontSize.md}>
{item.title}
</Heading>
{/* <Paragraph
style={{ marginLeft: 10 }}
size={SIZE.sm}
color={colors.secondary.paragraph}
>
{item.desc}
</Paragraph> */}
</View>
</Pressable>
</Fragment>
))
) : (
<View
style={{
minHeight: 150,
justifyContent: "center",
alignItems: "center",
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
{!complete ? (
<>
<ActivityIndicator />
<Paragraph>
{strings.exportingNotes(status) +
"..." +
" " +
strings.pleaseWait()}
</Paragraph>
</>
) : (
<>
<IconButton
name="export"
color={colors.primary.icon}
size={50}
style={{
width: 70,
height: 70
}}
/>
<Heading
style={{
textAlign: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
color={colors.secondary.heading}
>
{strings.exportSuccessHeading(ids.length)}
<Heading fontSize="MD" lineHeight="100%">
{item.title}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
>
{strings.exportSuccessDesc(result?.fileName as string)}
</Paragraph>
<Button
title={
Platform.OS === "android"
? strings.openFileLocation()
: strings.open()
}
type="accent"
width={250}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
onPress={async () => {
if (!result?.filePath) return;
close?.();
if (Platform.OS === "android") {
Linking.openURL(result.fileDir).catch((e) => {
ToastManager.error(e as Error);
});
} else {
await sleep(500);
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
ToastManager.show({
heading: strings.noApplicationFound(result.name),
type: "success",
context: "local"
});
});
}
}}
/>
<Button
title={strings.share()}
type="secondaryAccented"
width={250}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
onPress={async () => {
if (!result) return;
close?.();
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "ios") {
await sleep(500);
Share.open({
url: result?.fileDir + result.fileName
}).catch(() => {
/* empty */
});
} else {
FileViewer.open(result.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
} as any).catch(() => {
/* empty */
});
}
}}
/>
<Button
title={strings.exportAgain()}
type="secondaryAccented"
width={250}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
onPress={async () => {
setComplete(false);
setResult(undefined);
setExporting(false);
}}
/>
</>
)}
</Pressable>
))}
</View>
)}
</View>
</>
) : (
<View
style={{
minHeight: 150,
justifyContent: "center",
alignItems: "center",
width: "100%",
paddingVertical: Spacing.LEVEL_3,
gap: Spacing.LEVEL_2
}}
>
{!complete ? (
<>
<ActivityIndicator color={colors.primary.accent} />
<Paragraph>
{strings.exportingNotes(status) +
"..." +
" " +
strings.pleaseWait()}
</Paragraph>
</>
) : (
<>
<Button
title={
Platform.OS === "android"
? strings.openFileLocation()
: strings.open()
}
type="accent"
width={"100%"}
onPress={async () => {
if (!result?.filePath) return;
close?.();
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "android") {
Linking.openURL(result.fileDir).catch((e) => {
ToastManager.error(e as Error);
});
} else {
await sleep(500);
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch(() => {
ToastManager.show({
heading: strings.noApplicationFound(result.name),
type: "success",
context: "local"
});
});
}
}}
/>
<Button
title={strings.share()}
type="secondary-simple"
width={"100%"}
onPress={async () => {
if (!result) return;
close?.();
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "ios") {
await sleep(500);
Share.open({
url: result?.fileDir + result.fileName
}).catch(() => {
/* empty */
});
} else {
FileViewer.open(result.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
} as any).catch(() => {
/* empty */
});
}
}}
/>
<Button
title={strings.exportAgain()}
type="plain-outline"
width={"100%"}
onPress={async () => {
setComplete(false);
setResult(undefined);
setExporting(false);
}}
/>
</>
)}
</View>
)}
</View>
);
};
@@ -372,38 +368,4 @@ ExportNotesSheet.present = async (ids?: string[], allNotes?: boolean) => {
});
};
const styles = StyleSheet.create({
container: {
...getElevationStyle(5),
borderRadius: defaultBorderRadius,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
},
buttonContainer: {
justifyContent: "space-between",
alignItems: "center"
},
button: {
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP,
marginTop: DefaultAppStyles.GAP_VERTICAL,
borderRadius: defaultBorderRadius,
width: "100%",
justifyContent: "center",
alignItems: "center",
borderWidth: 1,
flexDirection: "row"
},
buttonText: {
//fontFamily: "sans-serif",
color: "white",
fontSize: AppFontSize.sm,
marginLeft: 5
},
overlay: {
width: "100%",
height: "100%",
position: "absolute"
}
});
export default ExportNotesSheet;

View File

@@ -24,6 +24,7 @@ import { ToastManager } from "../../../services/event-manager";
import { Button } from "../../ui/button";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import { useSettingStore } from "../../../stores/use-setting-store";
export const ShareComponent = ({ uri, name, padding }) => {
return (
<View
@@ -36,6 +37,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
type="accent"
width="100%"
onPress={async () => {
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
FileViewer.open(uri, {
showOpenWithDialog: true,
showAppsSuggestions: true
@@ -56,6 +58,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
onPress={async () => {
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
FileViewer.open(uri, {
showOpenWithDialog: true,
showAppsSuggestions: true,

View File

@@ -0,0 +1,221 @@
/*
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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { View } from "react-native";
import { Radius, Spacing } from "../../../common/design/spacing";
import { Button } from "../../ui/button";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { AppFontSize } from "../../../utils/size";
import { presentSheet, eSendEvent } from "../../../services/event-manager";
import { eCloseSheet } from "../../../utils/events";
import { sleep } from "../../../utils/time";
import { Progress } from "../progress";
import Sync from "../../../services/sync";
type ForceSyncMode = "fetch" | "send";
type ForceSyncProps = {
mode: ForceSyncMode;
close?: (ctx?: string) => void;
};
function ForceSync({ mode, close }: ForceSyncProps) {
const { colors } = useThemeColors();
const [acknowledged, setAcknowledged] = useState(true);
const onForceSyncPress = async () => {
if (!acknowledged) return;
close?.();
await sleep(300);
Progress.present();
Sync.run("global", true, mode, () => {
eSendEvent(eCloseSheet);
});
};
const title =
mode === "fetch" ? strings.forcePullChanges() : strings.forcePushChanges();
const description =
mode === "fetch"
? strings.forceSyncPullSheetDesc()
: strings.forceSyncPushSheetDesc();
const actionText =
mode === "fetch" ? strings.forcePullAction() : strings.forcePushAction();
return (
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.error.background
}}
>
<AppIcon
name="warning"
iconFamily="notesnook"
size={16}
color={colors.error.accent}
/>
</View>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_0
}}
>
<Heading size={AppFontSize.lg} lineHeight="100%">
{title}
</Heading>
<Paragraph size={AppFontSize.xs}>{description}</Paragraph>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: colors.primary.border
}}
/>
<View
style={{
gap: Spacing.LEVEL_2
}}
>
<Paragraph size={AppFontSize.sm}>
{strings.forceSyncWarningShort()}
</Paragraph>
<View
style={{
gap: Spacing.LEVEL_0
}}
>
<Paragraph size={AppFontSize.sm}>
{strings.forceSyncNeedHelpContact()}
</Paragraph>
<Paragraph
size={AppFontSize.sm}
fontFamily="SEMI_BOLD"
color={colors.primary.accent}
>
support@streetwriters.co
</Paragraph>
</View>
</View>
<Pressable
type="shade"
style={{
width: "100%",
borderRadius: Radius.XS,
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_1,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1,
justifyContent: "flex-start"
}}
onPress={() => setAcknowledged((value) => !value)}
>
<AppIcon
size={16}
name={acknowledged ? "checkbox" : "checkbox-blank-outline"}
iconFamily="notesnook"
color={
acknowledged
? [colors.primary.accent, "white"]
: colors.primary.icon
}
/>
<Paragraph size={AppFontSize.sm}>
{strings.forceSyncRiskAcknowledgement()}
</Paragraph>
</Pressable>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2
}}
>
<Button
title={strings.cancel()}
type="plain-outline"
width="100%"
style={{
flex: 1,
borderRadius: Radius.S
}}
onPress={() => close?.()}
/>
<Button
title={actionText}
type="error"
width="100%"
disabled={!acknowledged}
style={{
flex: 1,
borderRadius: Radius.S,
borderColor: colors.error.border
}}
onPress={onForceSyncPress}
/>
</View>
</View>
);
}
ForceSync.present = (mode: ForceSyncMode) => {
presentSheet({
disableClosing: false,
component: (_ref, close) => <ForceSync mode={mode} close={close} />
});
};
export default ForceSync;

View File

@@ -18,26 +18,33 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Debug, IssueReportResponse } from "@notesnook/core";
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
import { strings } from "@notesnook/intl";
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 { Linking, Platform, View } from "react-native";
import Config from "react-native-config";
import {
getBrand,
getModel,
getSystemVersion,
getVersion
} from "react-native-device-info";
import { Radius, Spacing } from "../../../common/design/spacing";
import { useStoredRef } from "../../../hooks/use-stored-ref";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSheet } from "../../../utils/events";
import { openLinkInBrowser } from "../../../utils/functions";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size/index";
import DialogHeader from "../../dialog/dialog-header";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import {
createFormRef,
FormInput,
validators
} from "../../ui/input/form-input";
import Heading from "../../ui/typography/heading";
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,
@@ -54,22 +61,27 @@ export const Issue = ({
const [done, setDone] = useState(false);
const user = useUserStore((state) => state.user);
const [loading, setLoading] = useState(false);
const bodyRef = useRef<TextInput>(null);
const initialLayout = useRef(false);
const formRef = useRef(
createFormRef({
title: title.current || "",
body: body.current || ""
})
);
const issueReportResponse = useRef<IssueReportResponse>(undefined);
const onPress = async () => {
if (loading) return;
if (!title.current || !body.current) return;
if (title.current?.trim() === "" || body.current?.trim().length === 0)
return;
if (!formRef.current.validate()) return;
const titleValue = formRef.current.getValue("title").trim();
const bodyValue = formRef.current.getValue("body").trim();
try {
setLoading(true);
issueReportResponse.current = await Debug.report({
title: title.current,
title: titleValue,
body:
body.current +
bodyValue +
`\n${defaultBody || ""}` +
`\n_______________
**Device information:**
@@ -93,6 +105,8 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
setLoading(false);
body.reset();
title.reset();
formRef.current.setValue("title", "");
formRef.current.setValue("body", "");
setDone(true);
} catch (e) {
setLoading(false);
@@ -135,161 +149,195 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
const responseInfo = getResponseInfo(issueReportResponse.current);
console.log(responseInfo, issueReportResponse.current);
return (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
width: "100%"
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3
}}
>
{done ? (
<View
style={{
justifyContent: "center",
alignItems: "center",
gap: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_3
}}
>
<Heading>{responseInfo?.title}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
selectable={true}
>
{responseInfo?.message}
</Paragraph>
<Button
title={responseInfo?.positiveButtonText || strings.done()}
onPress={() => {
if (responseInfo?.url) {
Linking.openURL(responseInfo?.url);
}
eSendEvent(eCloseSheet);
}}
type="accent"
width="100%"
/>
</View>
) : (
<>
<View
style={{
justifyContent: "center",
alignItems: "center",
gap: 10
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<Heading>{responseInfo?.title}</Heading>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.error.background
}}
>
<AppIcon
name="warning-circle"
iconFamily="notesnook"
size={16}
color={colors.error.accent}
/>
</View>
<View
style={{
flex: 1,
justifyContent: "center",
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{issueTitle || strings.issueTitle()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{issueTitle ? strings.issueDesc() : strings.issueDesc2()}
</Paragraph>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: colors.primary.border
}}
/>
<View
style={{
gap: Spacing.LEVEL_3
}}
>
<View
style={{
gap: Spacing.LEVEL_2
}}
>
<FormInput
name="title"
formRef={formRef}
label={strings.issueSummary()}
placeholder={strings.issueTitlePlaceholder()}
defaultValue={title.current}
validators={[validators.required(strings.allFieldsRequired())]}
onChangeText={(v) => (title.current = v)}
containerStyle={{
borderRadius: Radius.XS
}}
/>
<FormInput
name="body"
formRef={formRef}
label={strings.details()}
placeholder={strings.issuePlaceholder()}
multiline
numberOfLines={5}
textAlignVertical="top"
validators={[validators.required(strings.allFieldsRequired())]}
onChangeText={(v) => (body.current = v)}
containerStyle={{
borderRadius: Radius.XS,
alignItems: "flex-start"
}}
inputStyle={{
minHeight: 100
}}
/>
</View>
<Button
onPress={onPress}
title={loading ? null : strings.submit()}
loading={loading}
width="100%"
type="error"
style={{
borderRadius: Radius.S,
borderColor: colors.error.border
}}
/>
<Paragraph
color={colors.secondary.paragraph}
fontSize="XS"
style={{
textAlign: "center"
}}
selectable={true}
>
{responseInfo?.message}
</Paragraph>
<Button
title={responseInfo?.positiveButtonText || "Done"}
onPress={() => {
if (responseInfo?.url) {
Linking.openURL(responseInfo?.url);
}
eSendEvent(eCloseSheet);
}}
type="accent"
width="100%"
/>
</View>
</>
) : (
<>
<DialogHeader
title={issueTitle || strings.issueTitle()}
paragraph={issueTitle ? strings.issueDesc() : strings.issueDesc2()}
/>
<Seperator half />
<TextInput
placeholder={strings.title()}
onChangeText={(v) => (title.current = v)}
defaultValue={title.current}
style={{
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: defaultBorderRadius,
padding: DefaultAppStyles.GAP,
fontFamily: "Inter-Regular",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
fontSize: AppFontSize.md,
color: colors.primary.heading
}}
placeholderTextColor={colors.primary.placeholder}
/>
<TextInput
ref={bodyRef}
multiline
placeholder={strings.issuePlaceholder()}
numberOfLines={5}
textAlignVertical="top"
onChangeText={(v) => (body.current = v)}
onLayout={() => {
if (initialLayout.current) return;
initialLayout.current = true;
if (body.current) {
bodyRef.current?.setNativeProps({
text: body.current,
selection: {
start: 0,
end: 0
{strings.issueNotice[0]()}{" "}
<Paragraph
onPress={() => {
Linking.openURL(
"https://github.com/streetwriters/notesnook/issues"
);
}}
fontSize="XS"
fontFamily="MEDIUM"
style={{
color: colors.primary.accent
}}
>
Github.
</Paragraph>{" "}
{strings.issueNotice[1]()}{" "}
<Paragraph
style={{
color: colors.primary.accent
}}
fontSize="XS"
fontFamily="MEDIUM"
onPress={async () => {
try {
await openLinkInBrowser("https://discord.gg/zQBK97EE22");
} catch (e) {
console.error(e);
}
});
}
}}
style={{
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: defaultBorderRadius,
padding: DefaultAppStyles.GAP,
fontFamily: "Inter-Regular",
maxHeight: 200,
fontSize: AppFontSize.sm,
marginBottom: 2.5,
color: colors.primary.paragraph
}}
placeholderTextColor={colors.primary.placeholder}
/>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>{`App version: ${getVersion()} Platform: ${
Platform.OS
} Model: ${getBrand()}-${getModel()}-${getSystemVersion()}`}</Paragraph>
<Seperator />
<Button
onPress={onPress}
title={loading ? null : strings.submit()}
loading={loading}
width="100%"
type="accent"
/>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
textAlign: "center"
}}
>
{strings.issueNotice[0]()}{" "}
<Text
onPress={() => {
Linking.openURL(
"https://github.com/streetwriters/notesnook/issues"
);
}}
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
>
github.com/streetwriters/notesnook.
</Text>{" "}
{strings.issueNotice[1]()}{" "}
<Text
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
onPress={async () => {
try {
await openLinkInBrowser("https://discord.gg/zQBK97EE22");
} catch (e) {
console.error(e);
}
}}
>
{strings.issueNotice[2]()}
</Text>
</Paragraph>
}}
>
{strings.issueNotice[2]()}
</Paragraph>
</Paragraph>
</View>
</>
)}
</View>

View File

@@ -0,0 +1,188 @@
/*
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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { useWindowDimensions, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import { presentSheet } from "../../../services/event-manager";
import { useSettingStore } from "../../../stores/use-setting-store";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const OPTIONS = [
1000 * 60 * 1,
1000 * 60 * 5,
1000 * 60 * 10,
1000 * 60 * 15,
1000 * 60 * 30,
1000 * 60 * 45,
1000 * 60 * 60,
-1
];
const formatValue = (item: number) => {
return item === -1
? strings.never()
: item < 1000 * 60 * 60
? strings.minutes(item / (1000 * 60))
: strings.hours(item / (1000 * 60 * 60));
};
type LockVaultTimerProps = {
close?: (ctx?: string) => void;
};
function LockVaultTimer({ close }: LockVaultTimerProps) {
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const [currentValue, setCurrentValue] = useState(
useSettingStore.getState().vaultLockAfter
);
const onChange = async (item: number) => {
await db.settings.setVaultLockAfter(item);
useSettingStore.setState({
vaultLockAfter: item
});
setCurrentValue(item);
close?.();
};
return (
<ScrollView
style={{
maxHeight: height * 0.8
}}
>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
paddingTop: Spacing.LEVEL_2
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="clock"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{strings.lockVaultAfter()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.lockVaultAfterDesc()}
</Paragraph>
</View>
</View>
</View>
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_2
}}
>
{OPTIONS.map((item) => {
const selected = currentValue === item;
return (
<Pressable
key={item}
type={selected ? "selected" : "transparent"}
onPress={() => onChange(item)}
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: selected ? 0 : 1,
borderColor: colors.primary.border
}}
>
<Heading
fontFamily="MEDIUM"
fontSize="SM"
lineHeight="100%"
color={colors.primary.heading}
>
{formatValue(item)}
</Heading>
<AppIcon
name={selected ? "radio-button" : "ellipse"}
iconFamily="notesnook"
size={16}
color={
selected
? [colors.selected.accent, colors.static.white]
: colors.secondary.icon
}
/>
</Pressable>
);
})}
</View>
</ScrollView>
);
}
LockVaultTimer.present = () => {
presentSheet({
component: (_ref, close) => <LockVaultTimer close={close} />
});
};
export default LockVaultTimer;

View File

@@ -37,8 +37,8 @@ import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSheet } from "../../../utils/events";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { FontSizes } from "../../../common/design/font";
import { Radius, Spacing } from "../../../common/design/spacing";
import { AuthMode } from "../../auth/common";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
@@ -87,181 +87,188 @@ export default function PaywallSheet<Tid extends FeatureId>(props: {
SubscriptionProvider.GOOGLE &&
Platform.OS === "android");
return !pricingPlans.currentPlan ? null : (
if (!pricingPlans.currentPlan) return null;
const features = [
{
title: `${
PlanOverView[pricingPlans.currentPlan.id as keyof typeof PlanOverView]
.storage
} ${strings.cloudStorage()}`,
description: strings.cloudStorageBenefit()
},
...(pricingPlans.currentPlan.id !== "essential"
? [
{
title: strings.appLockSecurity(),
description: strings.appLockSecurityBenefit()
}
]
: []),
{
title: strings.advancedNoteElements(),
description: strings.advancedNoteElementsBenefit()
}
];
const onUpgrade = () => {
if (PremiumService.get()) {
if (
pricingPlans.user?.subscription.plan === SubscriptionPlan.LEGACY_PRO ||
!isCurrentPlatform
) {
ToastManager.show({
message: strings.cannotChangePlan(),
context: "local"
});
return;
}
if (isSubscribedOnWeb) {
ToastManager.show({
message: strings.changePlanOnWeb(),
context: "local"
});
return;
}
}
eSendEvent(eCloseSheet);
if (!useUserStore.getState().user) {
Navigation.navigate("Auth", {
mode: AuthMode.login
});
return;
}
Navigation.navigate("PayWall", {
context: "logged-in",
state: {
planId: pricingPlans.currentPlan?.id,
productId: isGithubRelease
? "yearly"
: (pricingPlans.selectProduct as any).productId,
billingType: "annual"
}
});
};
return (
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
gap: DefaultAppStyles.GAP_VERTICAL
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_4,
gap: Spacing.LEVEL_3
}}
>
<>
<View style={{ gap: Spacing.LEVEL_3, width: "100%" }}>
<View
style={{
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
width: "100%"
}}
>
<Paragraph>
<AppIcon
name="crown"
size={AppFontSize.md}
color={colors.static.orange}
/>
{strings.upgradePlanTo(pricingPlans.currentPlan?.name)}
</Paragraph>
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL_SMALL,
width: "100%"
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<Heading>{strings.tryItForFree()}</Heading>
<Heading size={AppFontSize.sm}>
{strings.getThisAndSoMuchMore()}
<AppIcon
name="crown-simple"
iconFamily="notesnook"
size={16}
color={colors.static.orange}
/>
</View>
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
<Heading fontSize="XL" lineHeight="100%">
{strings.tryItForFree()}
</Heading>
<View
style={{
gap: DefaultAppStyles.GAP_SMALL,
flexDirection: "row"
}}
>
<AppIcon name="cloud" size={AppFontSize.xxl} />
<Paragraph
style={{
flexShrink: 1
}}
>
<Heading size={AppFontSize.sm}>
{
PlanOverView[
pricingPlans.currentPlan.id as keyof typeof PlanOverView
].storage
}
</Heading>{" "}
{strings.cloudSpace()}
</Paragraph>
</View>
{pricingPlans.currentPlan.id !== "essential" ? (
<View
style={{
gap: DefaultAppStyles.GAP_SMALL,
flexDirection: "row"
}}
>
<AppIcon name="lock" size={AppFontSize.xxl} />
<Paragraph
style={{
flexShrink: 1
}}
>
<Heading size={AppFontSize.sm}>{strings.appLock()}</Heading>{" "}
{strings.appLockFeatureBenefit()}
</Paragraph>
</View>
) : null}
<View
style={{
gap: DefaultAppStyles.GAP_SMALL,
flexDirection: "row"
}}
>
<AppIcon name="vector-link" size={AppFontSize.xxl} />
<Paragraph
style={{
flexShrink: 1
}}
>
{strings.advancedNoteTaking[0]()}{" "}
<Heading size={AppFontSize.sm}>
{strings.advancedNoteTaking[1]()}
</Heading>{" "}
{strings.advancedNoteTaking[2]()}
</Paragraph>
</View>
<Paragraph fontSize="SM" color={colors.primary.paragraph}>
{strings.unlockPremiumFeatures()}
</Paragraph>
</View>
</View>
<View
style={{
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph
style={{
marginVertical: 10
}}
size={AppFontSize.xs}
>
<Heading size={AppFontSize.xs}>{strings.cancelAnytime()}</Heading>{" "}
{strings.googleReminderTrial()}
</Paragraph>
<View style={{ height: 1, backgroundColor: colors.primary.border }} />
<Button
type="accent"
title={strings.upgrade()}
style={{
marginVertical: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
onPress={() => {
if (PremiumService.get()) {
if (
pricingPlans.user?.subscription.plan ===
SubscriptionPlan.LEGACY_PRO ||
!isCurrentPlatform
) {
ToastManager.show({
message: strings.cannotChangePlan(),
context: "local"
});
return;
}
if (isSubscribedOnWeb) {
ToastManager.show({
message: strings.changePlanOnWeb(),
context: "local"
});
return;
}
}
eSendEvent(eCloseSheet);
if (!useUserStore.getState().user) {
Navigation.navigate("Auth", {
mode: AuthMode.login
});
return;
}
Navigation.navigate("PayWall", {
context: "logged-in",
state: {
planId: pricingPlans.currentPlan?.id,
productId: isGithubRelease
? "yearly"
: (pricingPlans.selectProduct as any).productId,
billingType: "annual"
}
});
}}
/>
<View style={{ gap: Spacing.LEVEL_2, width: "100%" }}>
{features.map((feature) => (
<View
key={feature.title}
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
width: "100%"
}}
>
<AppIcon
name="check"
iconFamily="notesnook"
size={16}
color={colors.primary.accent}
style={{ marginTop: 1 }}
/>
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
<Heading fontFamily="MEDIUM" fontSize="SM" lineHeight="100%">
{feature.title}
</Heading>
<Paragraph fontSize="XS" color={colors.primary.paragraph}>
{feature.description}
</Paragraph>
</View>
</View>
))}
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0 + 2,
padding: Spacing.LEVEL_1,
borderRadius: Radius.XS,
backgroundColor: colors.selected.background
}}
>
<Paragraph fontSize="XS" color={colors.secondary.heading}>
{strings.sevenDayFreeTrial()}
</Paragraph>
<View
style={{
width: 4,
height: 4,
borderRadius: Radius.MD,
backgroundColor: colors.secondary.heading
}}
/>
<Paragraph fontSize="XS" color={colors.secondary.heading}>
{strings.cancelAnytimeShort()}
</Paragraph>
</View>
</View>
<View
style={{ flexDirection: "row", gap: Spacing.LEVEL_2, width: "100%" }}
>
{isSubscribedOnWeb ? null : (
<Button
type="plain"
title={strings.exploreAllPlans()}
icon="arrow-right"
iconPosition="right"
type="plain-outline"
title={strings.viewPlans()}
fontSize={FontSizes.MD}
style={{
flex: 1,
borderRadius: Radius.S,
paddingVertical: Spacing.LEVEL_3
}}
onPress={() => {
eSendEvent(eCloseSheet);
Navigation.navigate("PayWall", {
@@ -272,7 +279,18 @@ export default function PaywallSheet<Tid extends FeatureId>(props: {
}}
/>
)}
</>
<Button
type="accent"
title={strings.upgrade()}
fontSize={FontSizes.MD}
style={{
flex: 1,
borderRadius: Radius.S,
paddingVertical: Spacing.LEVEL_3
}}
onPress={onUpgrade}
/>
</View>
</View>
);
}

View File

@@ -16,30 +16,40 @@ 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 from "react";
import React, { useEffect, useState } from "react";
import {
FeatureId,
FeatureUsage,
formatBytes,
getFeature,
getFeaturesUsage
} from "@notesnook/common";
import { SubscriptionPlan, SubscriptionProvider } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { useEffect, useState } from "react";
import { Platform, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { FontSizes } from "../../../common/design/font";
import { Spacing, Radius } from "../../../common/design/spacing";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSheet } from "../../../utils/events";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { SubscriptionPlan, SubscriptionProvider } from "@notesnook/core";
import { useUserStore } from "../../../stores/use-user-store";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
const FEATURE_ICONS: Partial<Record<FeatureId, string>> = {
storage: "cloud",
colors: "palette",
tags: "shopping-mode",
notebooks: "book-open",
activeReminders: "bell",
shortcuts: "arrow-square-out"
};
export function PlanLimits() {
const { colors } = useThemeColors();
@@ -63,46 +73,97 @@ export function PlanLimits() {
return (
<ScrollView
style={{
paddingHorizontal: DefaultAppStyles.GAP,
width: "100%",
paddingVertical: DefaultAppStyles.GAP_VERTICAL
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_4
}}
contentContainerStyle={{
gap: DefaultAppStyles.GAP_VERTICAL
gap: Spacing.LEVEL_3
}}
>
<Heading>{strings.planLimits()}</Heading>
{featureUsage?.map((item) => (
<View style={{ gap: Spacing.LEVEL_3, width: "100%" }}>
<View
key={item.id}
style={{
gap: DefaultAppStyles.GAP_VERTICAL_SMALL,
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between"
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<Paragraph size={AppFontSize.sm}>
{getFeature(item.id).title}
</Paragraph>
<Paragraph size={AppFontSize.sm}>
{item.total === Infinity
? strings.unlimited()
: item.id === "storage"
? `${formatBytes(item.used)}/${formatBytes(
item.total
)} ${strings.used()}`
: `${item.used}/${item.total} ${strings.used()}`}
<AppIcon
name="chart-donut"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
<Heading fontSize="XL" lineHeight="100%">
{strings.planLimits()}
</Heading>
<Paragraph fontSize="SM" color={colors.primary.paragraph}>
{strings.planLimitsDesc()}
</Paragraph>
</View>
</View>
))}
<View style={{ height: 1, backgroundColor: colors.primary.border }} />
<View style={{ gap: Spacing.LEVEL_2, width: "100%" }}>
{featureUsage?.map((item) => (
<View
key={item.id}
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
<AppIcon
name={FEATURE_ICONS[item.id] || "checkbox"}
iconFamily="notesnook"
size={16}
color={colors.secondary.icon}
/>
<Paragraph fontSize="SM" color={colors.primary.paragraph}>
{getFeature(item.id).title}
</Paragraph>
</View>
<Paragraph
fontFamily="MEDIUM"
fontSize="XS"
color={colors.secondary.heading}
>
{item.total === Infinity
? strings.unlimited()
: item.id === "storage"
? `${formatBytes(item.used)}/${formatBytes(
item.total
)} ${strings.used()}`
: `${item.used}/${item.total} ${strings.used()}`}
</Paragraph>
</View>
))}
</View>
</View>
{((user?.subscription?.provider === SubscriptionProvider.PADDLE ||
user?.subscription?.provider === SubscriptionProvider.STREETWRITERS ||
@@ -139,10 +200,10 @@ export function PlanLimits() {
eSendEvent(eCloseSheet);
}}
type="accent"
fontSize={AppFontSize.xs}
fontSize={FontSizes.MD}
style={{
width: "100%",
marginTop: DefaultAppStyles.GAP_VERTICAL
borderRadius: Radius.S
}}
/>
)}

View File

@@ -290,7 +290,7 @@ const PublishNoteSheet = ({
{strings.publishedAt()}:
</Heading>
<Paragraph size={AppFontSize.sm} numberOfLines={1}>
{publishUrl}
{monographMetadata?.publishUrl}
</Paragraph>
</View>
@@ -432,7 +432,7 @@ const PublishNoteSheet = ({
{isFeatureAvailable?.isAllowed &&
!selfDestruct &&
monographMetadata &&
monographMetadata?.analytics?.totalViews > 0 ? (
monographMetadata?.analytics.totalViews > 0 ? (
<View
style={{
flexDirection: "row",
@@ -457,7 +457,7 @@ const PublishNoteSheet = ({
>
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
<Paragraph>
{monographMetadata?.analytics?.totalViews || 0}
{monographMetadata?.analytics.totalViews || 0}
</Paragraph>
</View>
</View>

View File

@@ -1,338 +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 { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { createRef } from "react";
import { PermissionsAndroid, Platform, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import { db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import {
eSubscribeEvent,
eUnSubscribeEvent,
ToastManager
} from "../../../services/event-manager";
import { clearMessage } from "../../../services/message";
import SettingsService from "../../../services/settings";
import { eOpenRecoveryKeyDialog } from "../../../utils/events";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import { QRCode } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
class RecoveryKeySheet extends React.Component {
constructor(props) {
super(props);
this.state = {
key: null,
visible: false
};
this.actionSheetRef = createRef();
this.svg = createRef();
this.user;
this.signup = false;
this.tapCount = 0;
}
open = (signup) => {
if (signup) {
this.signup = true;
}
this.setState(
{
visible: true
},
() => {
this.actionSheetRef.current?.setModalVisible(true);
}
);
};
close = () => {
if (this.tapCount === 0) {
ToastManager.show({
heading: strings.recoveryKeySaved(),
message: strings.recoveryKeySavedDesc(),
type: "success",
context: "local"
});
this.tapCount++;
return;
}
this.tapCount = 0;
this.actionSheetRef.current?.setModalVisible(false);
sleep(200).then(() => {
this.setState({
visible: false
});
});
SettingsService.set({
recoveryKeySaved: true
});
clearMessage();
};
async componentDidMount() {
eSubscribeEvent(eOpenRecoveryKeyDialog, this.open);
}
async componentWillUnmount() {
eUnSubscribeEvent(eOpenRecoveryKeyDialog, this.open);
}
saveQRCODE = async () => {
this.svg.current?.toDataURL(async (data) => {
try {
let fileName =
"nn_" + this.user.email + "_recovery_key_qrcode" + "_" + Date.now();
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".png";
const path = RNFetchBlob.fs.dirs.CacheDir + fileName;
await RNFetchBlob.fs.writeFile(path, data, "base64");
await CameraRoll.saveToCameraRoll(`file://` + path);
ToastManager.show({
heading: strings.recoveryKeyQRCodeSaved(),
type: "success",
context: "local"
});
} catch (e) {
console.error(e);
}
});
};
saveToTextFile = async () => {
try {
let path;
let fileName = "nn_" + this.user?.email + "_recovery_key";
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".txt";
if (Platform.OS === "android") {
let file = await ScopedStorage.createDocument(
fileName,
"text/plain",
this.state.key,
"utf8"
);
if (!file) return;
path = file.uri;
} else {
path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, this.state.key, "utf8");
path = path + fileName;
}
ToastManager.show({
heading: strings.recoveryKeyTextFileSaved(),
type: "success",
context: "local"
});
return path;
} catch (e) {
alert(e.message);
}
};
onOpen = async () => {
let k = await db.user.getMasterKey();
this.user = await db.user.getUser();
if (k) {
this.setState({
key: k.key
});
}
};
shareFile = async () => {
let path = await this.saveToTextFile();
if (!path) return;
try {
if (Platform.OS === "ios") {
Share.open({
url: path,
failOnCancel: false
}).catch(() => {
/* empty */
});
} else {
FileViewer.open(path, {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
}).catch(() => {
/* empty */
});
}
} catch (e) {
console.error(e);
}
};
render() {
const { colors } = this.props;
if (!this.state.visible) return null;
return (
<SheetWrapper
closeOnTouchBackdrop={false}
gestureEnabled={false}
onOpen={this.onOpen}
fwdRef={this.actionSheetRef}
>
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
justifyContent: "space-between",
paddingHorizontal: DefaultAppStyles.GAP,
borderRadius: 10,
paddingTop: 10
}}
>
<DialogHeader
title={strings.saveRecoveryKey()}
paragraph={strings.saveRecoveryKeyDesc()}
/>
<View
style={{
borderRadius: defaultBorderRadius,
padding: DefaultAppStyles.GAP,
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph
color={colors.primary.paragraph}
size={AppFontSize.sm}
numberOfLines={2}
selectable
style={{
width: "100%",
maxWidth: "100%",
paddingRight: 10,
textAlign: "center",
textDecorationLine: "underline"
}}
>
{this.state.key}
</Paragraph>
</View>
<Seperator />
<View
style={{
alignSelf: "center",
marginBottom: 15,
flexDirection: "row",
width: "100%",
justifyContent: "center",
position: "absolute",
opacity: 0,
zIndex: -1
}}
>
{this.state.key ? (
<QRCode
getRef={this.svg}
size={500}
value={this.state.key}
//logo={{ uri: LOGO_BASE64 }}
logoBorderRadius={10}
/>
) : null}
</View>
<Button
onPress={() => {
Clipboard.setString(this.state.key);
ToastManager.show({
heading: strings.recoveryKeyCopied(),
type: "success",
context: "local"
});
}}
icon="content-copy"
title={strings.copyToClipboard()}
width="100%"
type="secondaryAccented"
/>
<Seperator />
<Button
title={strings.saveQRCode()}
onPress={this.saveQRCODE}
width="100%"
type="secondaryAccented"
icon="qrcode"
/>
<Seperator />
<Button
onPress={this.saveToTextFile}
title={strings.saveAsText()}
width="100%"
type="secondaryAccented"
icon="text"
/>
<Seperator />
<Button
onPress={this.shareFile}
title={strings.shareToCloud()}
width="100%"
type="secondaryAccented"
icon="cloud"
/>
<Seperator />
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.sm}
numberOfLines={2}
style={{
width: "100%",
maxWidth: "100%",
marginBottom: 5,
textAlign: "center"
}}
>
{strings.recoveryKeySavedConfirmation()}
</Paragraph>
<Button
title={strings.done()}
width="100%"
type="error"
onPress={this.close}
/>
</View>
</SheetWrapper>
);
}
}
export default RecoveryKeySheet;

View File

@@ -0,0 +1,454 @@
/*
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 { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import { Platform, ScrollView, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import { clearMessage } from "../../../services/message";
import SettingsService from "../../../services/settings";
import { AppFontSize } from "../../../utils/size";
import { Button } from "../../ui/button";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import { QRCode } from "../../ui/svg/lazy";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { User } from "@notesnook/core";
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
import { useSettingStore } from "../../../stores/use-setting-store";
type RecoveryKeySheetProps = {
close?: (ctx?: string) => void;
signup?: boolean;
};
type ActionItem = {
id: string;
icon: string;
title: string;
subtitle: string;
onPress: () => void | Promise<void>;
};
function RecoveryKeySheet({ close }: RecoveryKeySheetProps) {
const { colors } = useThemeColors();
const [key, setKey] = useState<string | null>(null);
const userRef = useRef<User>(undefined);
const svgRef = useRef<{
toDataURL: (callback: (data: string) => void) => void;
} | null>(null);
useEffect(() => {
const loadRecoveryData = async () => {
const masterKey = await db.user.getMasterKey();
userRef.current = await db.user.getUser();
if (masterKey?.key) {
setKey(masterKey.key);
}
};
void loadRecoveryData();
}, []);
const copyToClipboard = () => {
if (!key) return;
Clipboard.setString(key);
ToastManager.show({
heading: strings.recoveryKeyCopied(),
type: "success",
context: "local"
});
};
const saveQRCODE = async () => {
svgRef.current?.toDataURL(async (data) => {
try {
let fileName =
"nn_" +
userRef.current?.email +
"_recovery_key_qrcode" +
"_" +
Date.now();
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".png";
const path = RNFetchBlob.fs.dirs.CacheDir + fileName;
await RNFetchBlob.fs.writeFile(path, data, "base64");
await CameraRoll.saveToCameraRoll(`file://` + path);
ToastManager.show({
heading: strings.recoveryKeyQRCodeSaved(),
type: "success",
context: "local"
});
} catch (e) {
console.error(e);
}
});
};
const saveToTextFile = async () => {
if (!key) return;
try {
let path: string;
let fileName = `nn_${userRef.current?.email || "user"}_recovery_key`;
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = `${fileName}.txt`;
if (Platform.OS === "android") {
const file = await ScopedStorage.createDocument(
fileName,
"text/plain",
key,
"utf8"
);
if (!file) return;
path = file.uri;
} else {
path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, key, "utf8");
path = path + fileName;
}
ToastManager.show({
heading: strings.recoveryKeyTextFileSaved(),
type: "success",
context: "local"
});
return path;
} catch (error) {
alert((error as Error).message);
}
};
const shareFile = async () => {
const path = await saveToTextFile();
if (!path) return;
try {
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "ios") {
Share.open({
url: path,
failOnCancel: false
}).catch(() => {
/* empty */
});
} else {
FileViewer.open(path, {
showOpenWithDialog: true,
showAppsSuggestions: true,
//@ts-ignore
shareFile: true
}).catch(() => {
/* empty */
});
}
} catch (error) {
console.error(error);
}
};
const closeSheet = () => {
close?.();
SettingsService.set({ recoveryKeySaved: true });
clearMessage();
};
const actionItems: ActionItem[] = [
{
id: "copy-clipboard",
icon: "recovery-key-copy",
title: strings.copyToClipboard(),
subtitle: "Copy the recovery key to your clipboard",
onPress: copyToClipboard
},
{
id: "save-qr",
icon: "recovery-key-qr-code",
title: strings.saveQRCode(),
subtitle: "Save the QR code to your gallery",
onPress: saveQRCODE
},
{
id: "save-text",
icon: "recovery-key-file",
title: strings.saveAsText(),
subtitle: "Save the recovery key to a text file",
onPress: saveToTextFile
},
{
id: "share-cloud",
icon: "recovery-key-cloud-arrow-down",
title: strings.shareToCloud(),
subtitle: "Securely save the recovery key to cloud",
onPress: shareFile
}
];
return (
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_2,
maxHeight: "95%"
}}
>
<ScrollView bounces={false} style={{ width: "100%" }}>
<View style={{ gap: Spacing.LEVEL_0 }}>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: 6,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="recovery-key-key"
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
<Heading
size={AppFontSize.xl}
style={{
lineHeight: AppFontSize.xl
}}
fontFamily="SEMI_BOLD"
>
{strings.saveRecoveryKey()}
</Heading>
<Paragraph
size={AppFontSize.xs}
color={colors.primary.paragraph}
style={{
lineHeight: AppFontSize.xs * 1.2
}}
>
{strings.saveRecoveryKeyDesc()}
</Paragraph>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: colors.primary.border,
width: "100%",
marginVertical: Spacing.LEVEL_2
}}
/>
<View
style={{
backgroundColor: colors.secondary.background,
borderRadius: Radius.S,
padding: Spacing.LEVEL_2,
gap: Spacing.LEVEL_2
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
<AppIcon
name="recovery-key-shield-check"
iconFamily="notesnook"
size={16}
color={colors.primary.accent}
/>
<Paragraph
size={AppFontSize.sm}
color={colors.primary.accent}
style={{
lineHeight: AppFontSize.sm
}}
>
{strings.yourRecoveryKey()}
</Paragraph>
</View>
<View
style={{
backgroundColor: colors.tertiary.background,
borderRadius: Radius.XS,
borderWidth: 1.3,
borderStyle: "dashed",
borderColor: colors.primary.accent,
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_3,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: Spacing.LEVEL_2
}}
>
<Paragraph
size={AppFontSize.sm}
color={colors.primary.paragraph}
selectable
style={{
lineHeight: AppFontSize.sm * 1.2,
flexShrink: 1
}}
>
{key || ""}
</Paragraph>
<Button
icon="recovery-key-copy"
iconFamily="notesnook"
title={strings.copy()}
onPress={copyToClipboard}
fontSize={AppFontSize.sm}
style={{
paddingHorizontal: 0
}}
/>
</View>
</View>
<View
style={{
gap: Spacing.LEVEL_0
}}
>
{actionItems.map((item) => (
<Pressable
key={item.id}
onPress={item.onPress}
style={{
width: "100%",
borderRadius: Radius.S,
paddingVertical: Spacing.LEVEL_1,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name={item.icon}
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View style={{ flex: 1, gap: Spacing.LEVEL_0 }}>
<Heading size={AppFontSize.sm} fontFamily="MEDIUM">
{item.title}
</Heading>
<Paragraph
size={AppFontSize.xs}
color={colors.primary.paragraph}
>
{item.subtitle}
</Paragraph>
</View>
</Pressable>
))}
</View>
</View>
</ScrollView>
<Button
title={strings.done()}
width="100%"
type="accent"
onPress={closeSheet}
style={{
borderRadius: Radius.S
}}
/>
<View
style={{
position: "absolute",
left: -9999,
top: -9999,
opacity: 0
}}
>
{key ? (
<QRCode
getRef={(ref) => {
svgRef.current = ref;
}}
size={500}
value={key}
logoBorderRadius={10}
/>
) : null}
</View>
</View>
);
}
RecoveryKeySheet.present = (signup?: boolean) => {
presentSheet({
disableClosing: false,
component: (ref, close) => (
<RecoveryKeySheet close={close} signup={signup} />
)
});
};
export default RecoveryKeySheet;

View File

@@ -160,6 +160,33 @@ const Sort = ({
close();
};
useEffect(() => {
data?.groups?.().then((groups) => {
setGroups(groups);
offsets.current = [];
groups.map((item, index) => {
let offset = 35 * index;
let groupIndex = item.index;
const messageState = useMessageStore.getState().message;
const msgOffset = messageState?.visible ? 60 : 10;
groupIndex = groupIndex + 1;
groupIndex = groupIndex - (index + 1);
offset = offset + groupIndex * 100 + msgOffset;
offsets.current.push(offset);
});
const index = offsets.current?.findIndex((o, i) => {
return (
o <= currentScrollPosition.current + 100 &&
offsets.current[i + 1] - 100 > currentScrollPosition.current
);
});
setCurrentIndex(index < 0 ? 0 : index);
});
}, [data]);
return (
<View
style={{
@@ -265,7 +292,7 @@ const Sort = ({
{groupOptions?.sortBy === item ? (
<AppIcon
size={AppFontSize.lg}
name="checkbox"
name="radio-button"
iconFamily="notesnook"
color={[colors.selected.accent, "white"]}
/>

View File

@@ -0,0 +1,189 @@
/*
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 { isFeatureAvailable } from "@notesnook/common";
import { TrashCleanupInterval } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { useWindowDimensions, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import PaywallSheet from "../paywall";
import Paragraph from "../../ui/typography/paragraph";
const OPTIONS = [-1, 1, 7, 30, 365] as TrashCleanupInterval[];
const formatValue = (item: TrashCleanupInterval) => {
return item === -1
? strings.never()
: item === 1
? strings.reminderRecurringMode.day()
: strings.days(item);
};
type TrashIntervalProps = {
close?: (ctx?: string) => void;
};
function TrashInterval({ close }: TrashIntervalProps) {
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const [currentValue, setCurrentValue] = useState(
db.settings.getTrashCleanupInterval()
);
const onChange = async (item: TrashCleanupInterval) => {
const disableTrashFeature = await isFeatureAvailable("disableTrashCleanup");
if (!disableTrashFeature.isAllowed) {
ToastManager.show({
message: disableTrashFeature.error,
type: "info",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(disableTrashFeature);
}
});
}
db.settings.setTrashCleanupInterval(item);
setCurrentValue(item);
close?.();
};
return (
<ScrollView
style={{
maxHeight: height * 0.8
}}
>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
paddingTop: Spacing.LEVEL_2
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="trash"
iconFamily="notesnook"
size={16}
color={colors.static.red}
/>
</View>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{strings.clearTrashInterval()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.clearTrashIntervalDesc()}
</Paragraph>
</View>
</View>
</View>
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_2
}}
>
{OPTIONS.map((item) => {
const selected = currentValue === item;
return (
<Pressable
key={item}
type={selected ? "selected" : "transparent"}
onPress={() => onChange(item)}
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: selected ? 0 : 1,
borderColor: colors.primary.border
}}
>
<Heading
fontFamily="MEDIUM"
fontSize="SM"
lineHeight="100%"
color={colors.primary.heading}
>
{formatValue(item)}
</Heading>
<AppIcon
name={selected ? "radio-button" : "ellipse"}
iconFamily="notesnook"
size={16}
color={
selected
? [colors.selected.accent, colors.static.white]
: colors.secondary.icon
}
/>
</Pressable>
);
})}
</View>
</ScrollView>
);
}
TrashInterval.present = () => {
presentSheet({
component: (_ref, close) => <TrashInterval close={close} />
});
};
export default TrashInterval;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,306 @@
/*
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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useEffect, useState } from "react";
import {
ActivityIndicator,
Linking,
useWindowDimensions,
View
} from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import { checkVersion, CheckVersionResponse } from "react-native-check-version";
import Config from "react-native-config";
import deviceInfoModule from "react-native-device-info";
import { Radius, Spacing } from "../../../common/design/spacing";
import { STORE_LINK } from "../../../utils/constants";
import { GithubVersionInfo } from "../../../utils/github-version";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
type UpdateVersion = GithubVersionInfo | CheckVersionResponse;
type VersionInfo = {
version?: string | null;
needsUpdate?: boolean;
notes?: string;
body?: string;
};
type UpdateProps = {
version?: UpdateVersion;
fwdRef?: RefObject<ActionSheetRef>;
};
type IconTileProps = {
colors: ReturnType<typeof useThemeColors>["colors"];
name: string;
color?: string;
backgroundColor?: string;
};
const IconTile = ({ colors, name, color, backgroundColor }: IconTileProps) => (
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: backgroundColor || colors.secondary.background
}}
>
<AppIcon
name={name}
iconFamily="notesnook"
size={16}
color={color || colors.primary.icon}
/>
</View>
);
export const Update = ({ version: appVersion }: UpdateProps) => {
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const [version, setVersion] = useState<VersionInfo | undefined>(appVersion);
let notes = version?.notes
? version.notes.replace("Thank you for using Notesnook!", "").split("- ")
: ["Bug fixes and performance improvements"];
notes = notes?.map((n) => n.replace(/\n|<br>/g, ""));
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const getSupportedAbi = () => {
const abi = deviceInfoModule.supportedAbisSync();
const armv8a = abi.find((a) => a === "arm64-v8a");
const armv7 = abi.find((a) => a === "armeabi-v7a");
return armv8a || armv7 || abi[0];
};
const GITHUB_URL =
!version || !version.needsUpdate
? null
: `https://github.com/streetwriters/notesnook/releases/download/${
version.version
}-android/notesnook-${getSupportedAbi()}.apk`;
const GITHUB_PAGE_URL =
!version || !version.needsUpdate
? null
: `https://github.com/streetwriters/notesnook/releases/tag/${version.version}-android`;
useEffect(() => {
if (!version) {
(async () => {
try {
const v = await checkVersion();
setVersion(v);
} catch (e) {
setVersion({
needsUpdate: false
});
}
})();
}
}, [version]);
return (
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
borderTopLeftRadius: 35,
borderTopRightRadius: 35,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3
}}
>
{!version || !version?.needsUpdate ? (
<View
style={{
alignItems: "center",
justifyContent: "center",
gap: Spacing.LEVEL_2,
paddingBottom: Spacing.LEVEL_6
}}
>
{!version ? (
<>
<ActivityIndicator
style={{
marginTop: Spacing.LEVEL_4
}}
color={colors.primary.accent}
/>
<Paragraph fontSize="MD" color={colors.secondary.paragraph}>
{strings.checkNewVersion()}
</Paragraph>
</>
) : (
<>
<IconTile colors={colors} name="warning-circle" />
<View
style={{
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{strings.noUpdates()}
</Heading>
<Paragraph
fontSize="SM"
color={colors.secondary.paragraph}
style={{ textAlign: "center" }}
>
{strings.noUpdatesDesc()}
</Paragraph>
</View>
</>
)}
</View>
) : (
<>
<View
style={{
flexDirection: "column",
alignItems: "center",
gap: Spacing.LEVEL_2,
justifyContent: "center"
}}
>
<IconTile colors={colors} name="download-simple" />
<View
style={{
justifyContent: "center",
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="XL" lineHeight="100%">
{strings.updateAvailable()}
</Heading>
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.versionReleased(
version.version as string,
isGithubRelease ? "github" : "store"
)}
</Paragraph>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: colors.primary.border
}}
/>
<ScrollView
nestedScrollEnabled={true}
style={{
width: "100%",
maxHeight: height * 0.4
}}
>
<Heading fontSize="MD" lineHeight="100%">
{strings.releaseNotes()}
</Heading>
{version.body ? (
<Paragraph
color={colors.secondary.paragraph}
style={{
marginTop: Spacing.LEVEL_2,
marginBottom: Spacing.LEVEL_0,
fontFamily: "monospace",
fontSize: 12,
lineHeight: 20
}}
selectable
>
{version.body}
</Paragraph>
) : (
<View
style={{
marginTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_0
}}
>
{notes.map((item) =>
item && item !== "" ? (
<Paragraph
key={item}
color={colors.secondary.paragraph}
selectable
>
{`${item}`}
</Paragraph>
) : null
)}
</View>
)}
</ScrollView>
<Button
title={
isGithubRelease ? strings.downloadUpdate() : strings.update()
}
onPress={() => {
Linking.openURL(
(isGithubRelease ? GITHUB_URL : STORE_LINK) as string
).catch(console.log);
}}
type="accent"
style={{
width: "100%",
borderRadius: Radius.S
}}
/>
<Paragraph
fontSize="XS"
color={colors.secondary.paragraph}
style={{
textAlign: "center"
}}
onPress={() => {
Linking.openURL(GITHUB_PAGE_URL as string).catch(() => {
/* empty */
});
}}
>
{strings.readReleaseNotes[1]()}
<Paragraph
color={colors.primary.accent}
fontFamily="MEDIUM"
fontSize="XS"
>
{strings.readReleaseNotes[2]()}
</Paragraph>
</Paragraph>
</>
)}
</View>
);
};

View File

@@ -21,15 +21,14 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { useNetInfo } from "@react-native-community/netinfo";
import React from "react";
import { ActivityIndicator, Image, Linking, View } from "react-native";
import { Image, Linking, View } from "react-native";
import { useSheetRef } from "react-native-actions-sheet";
import useSyncProgress from "../../../hooks/use-sync-progress";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { SyncStatus, useUserStore } from "../../../stores/use-user-store";
import { getObfuscatedEmail } from "../../../utils/functions";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { AppFontSize } from "../../../utils/size";
import { AuthMode } from "../../auth/common";
import { Card } from "../../list/card";
import AppIcon from "../../ui/AppIcon";
@@ -37,10 +36,12 @@ import { Pressable } from "../../ui/pressable";
import { TimeSince } from "../../ui/time-since";
import Paragraph from "../../ui/typography/paragraph";
import Sync from "../../../services/sync";
import { Radius, Spacing } from "../../../common/design/spacing";
import Clipboard from "@react-native-clipboard/clipboard";
import { logoutUser } from "../../../screens/settings/logout";
import { sleep } from "../../../utils/time";
import Heading from "../../ui/typography/heading";
export const UserSheet = () => {
const ref = useSheetRef();
const { colors } = useThemeColors();
@@ -57,12 +58,100 @@ export const UserSheet = () => {
const { isInternetReachable } = useNetInfo();
const isOffline = !isInternetReachable;
const { progress } = useSyncProgress();
const canShowLastSyncedTime =
!!user &&
!!lastSynced &&
lastSynced !== "Never" &&
!syncing &&
lastSyncStatus !== SyncStatus.Failed;
const syncSubtitle = !user
? strings.notLoggedIn()
: syncing
? `${strings.syncing()}${progress ? ` (${progress.current})` : ""}${isOffline ? ` (${strings.offline()})` : ""}`
: lastSyncStatus === SyncStatus.Failed
? `${strings.syncFailed()}${isOffline ? ` (${strings.offline()})` : ""}`
: canShowLastSyncedTime
? `Last synced${isOffline ? ` (${strings.offline()})` : ""}`
: strings.never();
const actionItems = [
{
key: "sync",
icon: "user-sheet-sync",
title: strings.syncNow(),
subtitle: syncSubtitle,
onPress: () => {
if (!user) return;
Sync.run();
},
hidden: !user
},
{
key: "settings",
icon: "user-sheet-settings",
title: strings.settings(),
subtitle: "Preferences & app lock",
onPress: () => {
ref.current?.hide();
Navigation.navigate("Settings");
}
},
{
key: "support",
icon: "user-sheet-support",
title: strings.emailSupport(),
subtitle: "Response within 24 hours",
onPress: () => {
Clipboard.setString("support@streetwriters.co");
ToastManager.show({
heading: strings.emailCopied(),
type: "success",
icon: "content-copy",
context: "local"
});
setTimeout(() => {
Linking.openURL("mailto:support@streetwriters.co").catch((e) => {
ToastManager.show({
message: "Could not open email app",
type: "error",
context: "local"
});
});
}, 1000);
}
},
{
key: "documentation",
icon: "user-sheet-docs",
title: strings.documentation(),
subtitle: "Tutorials & help center",
onPress: async () => {
Linking.openURL("https://docs.notesnook.com");
}
},
{
key: "logout",
icon: "user-sheet-logout",
title: strings.logout(),
subtitle: "Sign out from this device",
onPress: async () => {
ref.current?.hide();
await sleep(300);
logoutUser();
},
hidden: !user
}
];
return (
<View
style={{
width: "100%",
justifyContent: "center"
justifyContent: "center",
backgroundColor: colors.primary.background,
paddingTop: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3
}}
>
{user ? (
@@ -70,8 +159,9 @@ export const UserSheet = () => {
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_SMALL
gap: Spacing.LEVEL_2,
width: "100%",
paddingHorizontal: Spacing.LEVEL_3
}}
>
{userProfile?.profilePicture ? (
@@ -80,77 +170,56 @@ export const UserSheet = () => {
uri: userProfile?.profilePicture
}}
style={{
width: 40,
height: 40,
borderRadius: defaultBorderRadius
width: 32,
height: 32,
borderRadius: Radius.XXL
}}
/>
) : null}
) : (
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XXL,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="account-outline"
size={16}
color={colors.secondary.icon}
/>
</View>
)}
<View
style={{
flex: 1,
flexDirection: "row",
justifyContent: "space-between"
flex: 1
}}
>
<View>
<Paragraph size={AppFontSize.xs}>
{userProfile?.fullName || getObfuscatedEmail(user.email)}
</Paragraph>
<View style={{ gap: Spacing.LEVEL_1 }}>
<Heading
style={{
fontSize: AppFontSize.lg,
lineHeight: AppFontSize.lg
}}
fontFamily="MEDIUM"
color={colors.primary.heading}
>
{userProfile?.fullName || strings.account()}
</Heading>
<Paragraph
style={{
flexWrap: "wrap"
fontSize: AppFontSize.xs,
lineHeight: AppFontSize.xs
}}
size={AppFontSize.xxs}
color={colors.secondary.heading}
color={colors.primary.paragraph}
>
{!user ? (
strings.notLoggedIn()
) : lastSynced && lastSynced !== "Never" ? (
<>
{syncing
? `${strings.syncing()} ${
progress ? `(${progress.current})` : ""
}`
: lastSyncStatus === SyncStatus.Failed
? strings.syncFailed()
: strings.synced()}{" "}
{!syncing ? (
<TimeSince
style={{
fontSize: AppFontSize.xxs,
color: colors.secondary.paragraph
}}
updateFrequency={30 * 1000}
time={lastSynced as number}
/>
) : null}
{isOffline ? ` (${strings.offline()})` : ""}
</>
) : (
strings.never()
)}{" "}
<AppIcon
name="checkbox-blank-circle"
size={10}
allowFontScaling
color={
!user || lastSyncStatus === SyncStatus.Failed
? colors.error.icon
: isOffline
? colors.static.orange
: colors.success.icon
}
/>
{getObfuscatedEmail(user.email)}
</Paragraph>
</View>
{syncing ? (
<ActivityIndicator
color={colors.primary.accent}
size={AppFontSize.xxl}
/>
) : null}
</View>
</View>
) : (
@@ -178,171 +247,96 @@ export const UserSheet = () => {
</View>
)}
{/* {user ? (
<View
style={{
paddingVertical: DefaultAppStyles.GAP_SMALL,
gap: DefaultAppStyles.GAP,
borderRadius: 10,
backgroundColor: colors.primary.background
}}
>
<View
style={{
gap: DefaultAppStyles.GAP_SMALL,
paddingHorizontal: DefaultAppStyles.GAP_SMALL
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between"
}}
>
<Paragraph size={AppFontSize.xxs}>{strings.storage()}</Paragraph>
<Paragraph size={AppFontSize.xxs}>
50/100MB {strings.used()}
</Paragraph>
</View>
<View
style={{
backgroundColor: colors.secondary.background,
width: "100%",
height: 5,
borderRadius: 10
}}
>
<View
style={{
backgroundColor: colors.static.black,
height: 5,
width: "50%",
borderRadius: 10
}}
/>
</View>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
padding: DefaultAppStyles.GAP_SMALL,
borderRadius: 10
}}
>
<View>
<Paragraph size={AppFontSize.sm}>{strings.freePlan()}</Paragraph>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xxxs}
>
{strings.viewAllLimits()}
<AppIcon name="information" size={AppFontSize.xxxs} />
</Paragraph>
</View>
<Button
title={strings.upgradeNow()}
onPress={() => {}}
type="accent"
fontSize={AppFontSize.xs}
style={{
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
height: "auto",
paddingVertical: DefaultAppStyles.GAP_SMALL
}}
/>
</View>
</View>
) : null} */}
<View
style={{
borderBottomWidth: 1,
height: 1,
width: "100%",
borderColor: colors.primary.border,
marginVertical: DefaultAppStyles.GAP_VERTICAL
paddingHorizontal: Spacing.LEVEL_3
}}
/>
>
<View
style={{
borderBottomWidth: 1,
height: 1,
width: "100%",
borderColor: colors.primary.border,
marginVertical: 0
}}
/>
</View>
<View>
{[
{
icon: "reload",
title: strings.syncNow(),
onPress: () => {
Sync.run();
},
hidden: !user
},
{
icon: "cog-outline",
title: strings.settings(),
onPress: () => {
ref.current?.hide();
Navigation.navigate("Settings");
}
},
{
title: strings.emailSupport(),
icon: "email",
onPress: () => {
Clipboard.setString("support@streetwriters.co");
ToastManager.show({
heading: strings.emailCopied(),
type: "success",
icon: "content-copy"
});
setTimeout(() => {
Linking.openURL("mailto:support@streetwriters.co");
}, 1000);
}
},
{
title: strings.documentation(),
onPress: async () => {
Linking.openURL("https://docs.notesnook.com");
},
icon: "file-document"
},
{
icon: "logout",
title: strings.logout(),
onPress: async () => {
ref.current?.hide();
await sleep(300);
logoutUser();
},
hidden: !user
}
].map((item) =>
<View style={{ gap: Spacing.LEVEL_0 }}>
{actionItems.map((item) =>
item.hidden ? null : (
<Pressable
key={item.title}
key={item.key}
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: Spacing.LEVEL_1,
alignItems: "center",
flexDirection: "row",
justifyContent: "flex-start",
gap: DefaultAppStyles.GAP_SMALL,
borderRadius: 0,
paddingHorizontal: DefaultAppStyles.GAP
gap: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_3
}}
onPress={() => {
item.onPress();
}}
>
<AppIcon
color={colors.secondary.icon}
name={item.icon}
size={AppFontSize.xl}
/>
<Paragraph>{item.title}</Paragraph>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
color={
item.key === "logout"
? colors.static.red
: colors.primary.icon
}
iconFamily="notesnook"
name={item.icon}
size={16}
/>
</View>
<View style={{ flex: 1, gap: Spacing.LEVEL_1 }}>
<Heading
style={{
fontSize: AppFontSize.md,
lineHeight: AppFontSize.md
}}
fontFamily="SEMI_BOLD"
>
{item.title}
</Heading>
<Paragraph
style={{
fontSize: AppFontSize.sm,
lineHeight: AppFontSize.sm
}}
color={colors.primary.paragraph}
>
{item.key === "sync" && canShowLastSyncedTime ? (
<>
{item.subtitle}{" "}
<TimeSince
style={{
fontSize: AppFontSize.sm,
color: colors.primary.paragraph
}}
updateFrequency={30 * 1000}
time={lastSynced as number}
/>
</>
) : (
item.subtitle
)}
</Paragraph>
</View>
</Pressable>
)
)}

View File

@@ -464,6 +464,11 @@ const TabBar = (props: SimpleTabBarProps) => {
presentSheet({
component: (
<Sort
dataType={
props.navigationState.index === 1
? "notebook"
: "tag"
}
type={
props.navigationState.index === 1
? "notebook"

View File

@@ -19,14 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
Platform,
TouchableOpacity,
useWindowDimensions,
View
} from "react-native";
import { TouchableOpacity, useWindowDimensions, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { Radius, Spacing } from "../../common/design/spacing";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useKeyboard from "../../hooks/use-keyboard";
import { DDS } from "../../services/device-detection";
@@ -37,8 +33,7 @@ import {
} from "../../services/event-manager";
import { getElevationStyle } from "../../utils/elevation";
import { eHideToast, eShowToast } from "../../utils/events";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { AppFontSize } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -130,14 +125,14 @@ export const Toast = ({ context = "global" }) => {
...getElevationStyle(5),
backgroundColor: isDark ? colors.static.black : colors.static.white,
alignSelf: "center",
borderRadius: defaultBorderRadius * 2,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP,
borderRadius: Radius.MD,
paddingVertical: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_2,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
maxWidth: "90%",
gap: DefaultAppStyles.GAP_SMALL,
gap: Spacing.LEVEL_1,
flexShrink: 1
}}
>
@@ -145,7 +140,7 @@ export const Toast = ({ context = "global" }) => {
style={{
flexDirection: "row",
alignItems: "center",
gap: DefaultAppStyles.GAP_SMALL,
gap: Spacing.LEVEL_1,
flexShrink: 1
}}
>

View File

@@ -27,7 +27,6 @@ import {
useWindowDimensions
} from "react-native";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import NativeTooltip from "../../../utils/tooltip";
import { Pressable, PressableProps, useButton } from "../pressable";
import Heading from "../typography/heading";
@@ -55,7 +54,7 @@ export interface ButtonProps extends PressableProps {
alpha?: number;
};
bold?: boolean;
iconColor?: ColorValue;
iconColor?: ColorValue | ColorValue[];
iconStyle?: TextStyle;
iconFamily?: IconProps["iconFamily"];
proTag?: boolean;
@@ -124,13 +123,12 @@ export const Button = ({
customOpacity={buttonType?.opacity}
customAlpha={buttonType?.alpha}
style={{
// height: typeof height === "number" ? height * growFactor : height,
width:
typeof width === "number"
? width * growFactor
: (width as DimensionValue) || undefined,
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_2,
borderRadius: defaultBorderRadius,
alignSelf: "center",
justifyContent: "center",

View File

@@ -0,0 +1,111 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View, ViewStyle } from "react-native";
import Svg, { Circle } from "react-native-svg";
type CirclesBackgroundProps = {
/** Width & height of the (square) illustration. */
size?: number;
/** Centered content, usually an `AppIcon`. */
children?: React.ReactNode;
style?: ViewStyle;
};
/**
* A reusable concentric-circles illustration with an empty center slot.
* Pass any icon/content via `children` to render it centered on the accent
* circle. Adapts to light/dark themes.
*/
export const CirclesBackground = ({
size = 100,
children,
style
}: CirclesBackgroundProps) => {
const { colors, isDark } = useThemeColors();
const accent = colors.primary.accent;
return (
<View
style={[
{
width: size,
height: size,
alignItems: "center",
justifyContent: "center"
},
style
]}
>
<Svg width={size} height={size} viewBox="0 0 233 233" fill="none">
<Circle
cx={116.5}
cy={116.5}
r={104.75}
stroke={isDark ? "#1F2722" : "#E8F4ED"}
strokeWidth={0.5}
/>
<Circle
cx={116.5}
cy={116.5}
r={95.25}
fill={accent}
fillOpacity={0.05}
stroke={isDark ? "#233C2D" : "#E8F0EC"}
strokeWidth={0.5}
/>
<Circle
cx={116.5}
cy={116.5}
r={82.25}
fill={accent}
fillOpacity={0.04}
stroke={isDark ? "#233C2D" : "#E3F1E8"}
strokeWidth={0.5}
/>
<Circle
cx={116.5}
cy={116.5}
r={62.25}
fill={accent}
fillOpacity={0.06}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<Circle cx={116.5} cy={116.5} r={44.5} fill={accent} />
</Svg>
{children ? (
<View
style={{
position: "absolute",
alignItems: "center",
justifyContent: "center"
}}
>
{children}
</View>
) : null}
</View>
);
};
export default CirclesBackground;

View File

@@ -386,7 +386,7 @@ export function FormInput({
>
{secureTextEntry && (
<IconButton
name={secureEntry ? "eye-closed" : "eye-open"}
name={!secureEntry ? "eye-closed" : "eye-open"}
iconFamily="notesnook"
size={20}
top={10}

View File

@@ -42,6 +42,7 @@ import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { IconButton } from "../icon-button";
import Paragraph from "../typography/paragraph";
import { useInputError } from "./input-error-context";
import { IconProps } from "../AppIcon";
interface InputProps extends TextInputProps {
fwdRef?: RefObject<TextInput | null>;
@@ -63,6 +64,7 @@ interface InputProps extends TextInputProps {
marginBottom?: number;
button?: {
icon: string;
iconFamily?: IconProps["iconFamily"];
color: ColorValue;
onPress: () => void;
testID?: string;
@@ -217,10 +219,7 @@ const Input = ({
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: Spacing.LEVEL_2,
paddingRight:
buttons || button || secureTextEntry || error
? Spacing.LEVEL_3
: Spacing.LEVEL_3,
paddingRight: Spacing.LEVEL_3,
...containerStyle
};
@@ -321,14 +320,12 @@ const Input = ({
<IconButton
testID={button.testID}
name={button.icon}
size={AppFontSize.xl}
iconFamily={button.iconFamily}
size={button.size || AppFontSize.xl}
top={10}
bottom={10}
onPress={button.onPress}
color={button.color}
style={{
marginRight: -8
}}
/>
)}
</View>

View File

@@ -20,9 +20,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View, ViewStyle } from "react-native";
import { Radius, Spacing } from "../../../common/design/spacing";
import { getContainerBorder } from "../../../utils/colors";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import AppIcon from "../AppIcon";
import Paragraph from "../typography/paragraph";
@@ -47,25 +47,40 @@ export const Notice = ({
return (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
padding: Spacing.LEVEL_2,
flexDirection: "row",
backgroundColor: colors.secondary.background,
borderRadius: isSmall ? 5 : 10,
backgroundColor:
type === "information"
? colors.primary.shade
: colors.secondary.background,
borderRadius: Radius.S,
alignItems: "flex-start",
gap: 5,
gap: Spacing.LEVEL_1,
...getContainerBorder(colors.secondary.background),
...style
}}
>
<AppIcon
size={isSmall ? AppFontSize.md + 2 : AppFontSize.xxl}
name={type}
color={type === "alert" ? colors.error.icon : colors.primary.accent}
<View
style={{
marginTop: isSmall ? 3 : 5
borderRadius: 100,
backgroundColor: colors.primary.accent,
width: 20,
height: 20,
justifyContent: "center",
alignItems: "center"
}}
/>
>
<AppIcon
size={12}
name={type === "information" ? "warning-circle" : type}
iconFamily="notesnook"
color={
type === "alert"
? colors.error.icon
: colors.primary.accentForeground
}
/>
</View>
<Paragraph
style={{
flexShrink: 1

View File

@@ -55,6 +55,7 @@ type ButtonTypes =
| "transparent"
| "accent"
| "shade"
| "shade-plain"
| "secondary"
| "tertiary"
| "selectedAccent"
@@ -65,7 +66,9 @@ type ButtonTypes =
| "errorShade"
| "warn"
| "selected"
| "accent-outline";
| "accent-outline"
| "secondary-outline"
| "secondary-simple";
type ButtonVariant = {
primary: string;
@@ -87,7 +90,7 @@ const buttonTypes = (
} => ({
plain: {
primary: "transparent",
text: colors.primary.buttonForeground,
text: colors.secondary.buttonForeground,
selected: colors.primary.hover,
borderWidth: 0.8,
borderSelectedColor: getColorLinearShade(
@@ -104,6 +107,14 @@ const buttonTypes = (
borderColor: colors.primary.border,
borderSelectedColor: colors.primary.border
},
"secondary-outline": {
primary: "transparent",
text: colors.secondary.paragraph,
selected: colors.primary.hover,
borderWidth: 1,
borderColor: colors.primary.border,
borderSelectedColor: colors.primary.border
},
transparent: {
primary: "transparent",
text: colors.primary.accent,
@@ -117,7 +128,7 @@ const buttonTypes = (
},
secondary: {
primary: colors.secondary.background,
text: colors.primary.paragraph,
text: colors.primary.heading,
selected: colors.secondary.background,
borderWidth: 0.8,
borderColor: getColorLinearShade(colors.secondary.background, 0.05, isDark),
@@ -127,6 +138,12 @@ const buttonTypes = (
isDark
)
},
"secondary-simple": {
primary: colors.secondary.background,
text: colors.primary.heading,
selected: colors.secondary.background,
borderWidth: 0
},
tertiary: {
primary: colors.tertiary.background,
text: colors.secondary.buttonForeground,
@@ -224,6 +241,15 @@ const buttonTypes = (
borderColor: getColorLinearShade(colors.primary.shade, 0.3, isDark),
borderSelectedColor: getColorLinearShade(colors.primary.shade, 0.3, isDark)
},
"shade-plain": {
primary: colors.primary.shade,
text: colors.primary.heading,
selected: colors.primary.accent,
colorOpacity: 0.12,
borderWidth: 0.8,
borderColor: getColorLinearShade(colors.primary.shade, 0.3, isDark),
borderSelectedColor: getColorLinearShade(colors.primary.shade, 0.3, isDark)
},
error: {
primary: colors.error.background,
text: colors.error.paragraph,

View File

@@ -132,7 +132,10 @@ const SheetWrapper = ({
}}
indicatorStyle={{
width: 100,
backgroundColor: colors.secondary.background
height: 5,
backgroundColor: colors.secondary.background,
marginBottom: 0,
marginTop: 0
}}
statusBarTranslucent
drawUnderStatusBar={true}

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { DimensionValue, View, ViewStyle } from "react-native";
import { SvgXml } from "./lazy";
import SvgImage from "react-native-svg/lib/typescript/elements/Image";
export const SvgView = ({
width = 250,
height = 250,

View File

@@ -63,7 +63,7 @@ import {
useTabStore
} from "../screens/editor/tiptap/use-tab-store";
import { editorController, editorState } from "../screens/editor/tiptap/utils";
import { useDragState } from "../screens/settings/editor/state";
import { useDragState } from "../screens/settings/components/editor/state";
import BackupService from "../services/backup";
import BiometricService from "../services/biometrics";
import {

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useAreFeaturesAvailable } from "@notesnook/common";
import { useEffect } from "react";
import { db } from "../common/database";
import { useDragState } from "../screens/settings/editor/state";
import { useDragState } from "../screens/settings/components/editor/state";
import Notifications from "../services/notifications";
import SettingsService from "../services/settings";
import { useUserStore } from "../stores/use-user-store";

View File

@@ -48,6 +48,9 @@ export const useVaultStatus = () => {
biometryEnrolled: fingerprint,
isBiometryAvailable: available ? true : false
});
VaultStatusDefaults.biometryEnrolled = fingerprint;
VaultStatusDefaults.exists = exists;
VaultStatusDefaults.isBiometryAvailable = available ? true : false;
});
}, []);

View File

@@ -28,5 +28,5 @@ const EditorMobileSourceUrl =
* The url should be something like this: http://192.168.100.126:3000/index.html
*/
export const EDITOR_URI = __DEV__
? "http://192.168.100.144:3000/index.html"
? EditorMobileSourceUrl
: EditorMobileSourceUrl;

View File

@@ -80,7 +80,7 @@ import { sleep } from "../../../utils/time";
import AddReminder from "../../add-reminder";
import ManageTags from "../../manage-tags";
import RelationsList from "../../relations-list";
import { useDragState } from "../../settings/editor/state";
import { useDragState } from "../../settings/components/editor/state";
import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { editorState, openInternalLink } from "./utils";

View File

@@ -1,825 +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 { 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,
useEffect,
useRef,
useState
} from "react";
import {
ActivityIndicator,
Linking,
Platform,
TextInput,
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";
import { db } from "../../common/database";
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 { Pressable } from "../../components/ui/pressable";
import Seperator from "../../components/ui/seperator";
import { SvgView } from "../../components/ui/svg";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import useTimer from "../../hooks/use-timer";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { DefaultAppStyles } from "../../utils/styles";
import PaywallSheet from "../../components/sheets/paywall";
const mfaMethods: MFAMethod[] = [
{
id: "app",
title: strings.mfaAuthAppTitle(),
body: strings.mfaAuthAppDesc(),
icon: "cellphone-key",
recommended: true
},
{
id: "sms",
title: strings.mfaSmsTitle(),
body: strings.mfaSmsDesc(),
icon: "message-plus-outline"
},
{
id: "email",
title: strings.mfaEmailTitle(),
body: strings.mfaEmailDesc(),
icon: "email-outline"
}
];
type MFAMethod = {
id: "email" | "sms" | "app";
title?: string;
body?: string;
icon?: string;
recommended?: boolean | undefined;
};
type MFAStep = {
id: string;
props: { [name: string]: unknown };
};
type MFAStepProps = {
recovery?: boolean;
onSuccess?: (method?: MFAMethod) => void;
setStep?: Dispatch<SetStateAction<MFAStep>>;
method?: MFAMethod;
isSetup?: boolean;
};
export const MFAMethodsPickerStep = ({ recovery, onSuccess }: MFAStepProps) => {
const { colors } = useThemeColors();
const user = useUserStore((state) => state.user);
const featureAvailable = useIsFeatureAvailable("sms2FA");
const getMethods = () => {
if (!recovery) return mfaMethods;
return mfaMethods.filter((m) => m.id !== user?.mfa?.primaryMethod);
};
return (
<>
<DialogHeader
title={strings.twoFactorAuth()}
paragraph={strings.twoFactorAuthDesc()}
padding={12}
/>
<Seperator />
{getMethods().map((item) => (
<Pressable
key={item.title}
onPress={() => {
if (
item.id === "sms" &&
featureAvailable &&
!featureAvailable?.isAllowed
) {
ToastManager.show({
message: featureAvailable?.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(featureAvailable);
}
});
return;
}
onSuccess && onSuccess(item);
}}
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
marginTop: 0,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
flexDirection: "row",
borderRadius: 0,
alignItems: "flex-start"
}}
>
{item.icon && (
<IconButton
type="secondary"
style={{
width: 50,
height: 50,
marginRight: 10
}}
size={20}
color={
item.recommended ? colors.primary.accent : colors.primary.icon
}
name={item.icon}
/>
)}
<View
style={{
flexShrink: 1
}}
>
<Heading size={AppFontSize.md}>{item.title}</Heading>
<Paragraph size={AppFontSize.sm}>{item.body}</Paragraph>
</View>
</Pressable>
))}
</>
);
};
export const MFASetup = ({
method,
onSuccess,
setStep,
recovery
}: 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 { 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>();
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;
}
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]);
const codeHelpText = {
app: "After putting the above code in authenticator app, the app will display a code that you can enter below.",
sms: "You will receive a 2FA code on your phone number which you can enter below",
email:
"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;
try {
if (!method) return;
const code = formRef.current.getValue("code").trim();
setGeneralError(undefined);
setEnabling(true);
if (recovery) {
await db.mfa.enableFallback(method.id, code);
} else {
await db.mfa.enable(method.id, code);
}
const user = await db.user.fetchUser();
useUserStore.getState().setUser(user);
onSuccess && onSuccess(method);
setEnabling(false);
} catch (e) {
const error = e as Error;
formRef.current.setError("code", error.message);
setEnabling(false);
}
};
const onSendCode = async () => {
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) {
await Linking.openURL(authenticatorDetails.authenticatorUri).catch(
console.log
);
}
ToastManager.show({
heading: strings.codesCopied(),
type: "success",
context: "local"
});
return;
}
try {
const target = formRef.current.getValue("target").trim();
setGeneralError(undefined);
if (seconds) {
setGeneralError(strings.resendCodeWait());
return;
}
setSending(true);
await db.mfa.setup(method.id, method.id === "sms" ? target : undefined);
if (method.id === "sms") {
setId(method.id + target);
}
await sleep(300);
start(60, method.id === "sms" ? method.id + target : 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);
}
}
};
return !method ? null : (
<View>
<DialogHeader
title={method?.title}
paragraph={method?.body}
padding={12}
/>
<Seperator />
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{loading ? (
<View
style={{
justifyContent: "center",
alignItems: "center",
width: "100%",
marginBottom: 50
}}
>
<ActivityIndicator
color={colors.primary.accent}
style={{
height: 50
}}
/>
<Paragraph>
{strings.gettingInformation()}... {strings.pleaseWait()}
</Paragraph>
</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
}
multiline={method.id === "app"}
onChangeText={() => {
setGeneralError(undefined);
}}
placeholder={
method.id === "email"
? strings.enterEmailAddress()
: "+1234567890"
}
onSubmitEditing={onSendCode}
validators={targetValidators}
keyboardType={
method.id === "email" ? "email-address" : "phone-pad"
}
buttons={
<Button
onPress={onSendCode}
loading={sending}
style={{
paddingVertical: 0,
paddingHorizontal: 0
}}
title={
sending
? null
: method.id === "app"
? strings.copy()
: `${
seconds
? strings.resendCode(seconds as number)
: strings.sendCode()
}`
}
/>
}
/>
<Heading size={AppFontSize.md}>
{strings.enterSixDigitCode()}
</Heading>
<Paragraph>{codeHelpText[method?.id]}</Paragraph>
<Seperator />
<FormInput
name="code"
formRef={formRef}
fwdRef={codeInputRef}
placeholder="xxxxxx"
maxLength={6}
loading={loading}
textAlign="center"
keyboardType="numeric"
onChangeText={() => {
setGeneralError(undefined);
}}
onSubmitEditing={onNext}
returnKeyLabel={strings.next()}
returnKeyType="done"
validators={codeValidators}
inputStyle={{
fontSize: AppFontSize.lg,
height: 60,
textAlign: "center",
letterSpacing: 10,
width: undefined
}}
containerStyle={{
height: 60,
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()}
type="accent"
width={250}
onPress={onNext}
loading={enabling}
style={{
borderRadius: 100,
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
/>
<Button
title={strings.change2faMethod()}
type="plain"
height={25}
onPress={() => {
setStep &&
setStep({
id: "mfapick",
props: {
recovery: recovery
}
});
}}
/>
</>
)}
</View>
</View>
);
};
export const MFARecoveryCodes = ({
method,
onSuccess,
isSetup = true
}: MFAStepProps) => {
const { colors } = useThemeColors();
const [codes, setCodes] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
try {
const codes = await db.mfa.codes();
if (codes) setCodes(codes);
setLoading(false);
} catch (e) {
const error = e as Error;
ToastManager.error(error, strings.errorGettingCodes(), "local");
setLoading(false);
}
})();
}, []);
return (
<View>
<DialogHeader
centered={true}
title={strings.saveRecoveryCodes()}
paragraph={strings.saveRecoveryCodesDesc()}
padding={12}
/>
<Seperator />
{loading ? (
<View
style={{
justifyContent: "center",
alignItems: "center",
width: "100%",
marginBottom: 50
}}
>
<ActivityIndicator
color={colors.primary.accent}
style={{
height: 50
}}
/>
<Paragraph>
{strings.gettingRecoveryCodes()}... {strings.pleaseWait()}
</Paragraph>
</View>
) : (
<>
<FlatList
data={codes}
contentContainerStyle={{
alignItems: "center"
}}
numColumns={2}
renderItem={({ item }) => (
<Heading
style={{
marginHorizontal: 15,
marginVertical: 5,
fontFamily: "monospace"
}}
size={AppFontSize.lg}
>
{item}
</Heading>
)}
/>
<Seperator />
<View
style={{
flexDirection: "row",
justifyContent: "center",
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
>
<Button
title={strings.copyCodes()}
onPress={() => {
const codeString = codes.join("\n");
Clipboard.setString(codeString);
ToastManager.show({
heading: strings.codesCopied(),
type: "success",
context: "local"
});
}}
style={{
marginRight: 10
}}
/>
<Button
title={strings.saveToFile()}
onPress={async () => {
try {
let path;
let fileName = "notesnook_recoverycodes";
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".txt";
const codeString = codes.join("\n");
if (Platform.OS === "android") {
const file = await ScopedStorage.createDocument(
fileName,
"text/plain",
codeString,
"utf8"
);
if (!file) return;
path = file.uri;
} else {
path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(
path + fileName,
codeString,
"utf8"
);
path = path + fileName;
}
ToastManager.show({
heading: strings.codesSaved(),
type: "success",
context: "local"
});
return path;
} catch (e) {
console.error(e);
}
}}
/>
</View>
<Button
title={isSetup ? strings.next() : strings.done()}
type="accent"
width={250}
onPress={() => {
if (isSetup) {
onSuccess && onSuccess(method);
} else {
eSendEvent(eCloseSheet);
}
}}
style={{
borderRadius: 100,
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
/>
</>
)}
</View>
);
};
MFARecoveryCodes.present = (methodId: MFAMethod["id"]) => {
presentSheet({
component: <MFARecoveryCodes method={{ id: methodId }} isSetup={false} />
});
};
const mfaSvg = (
colors: VariantsWithStaticColors
) => `<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 382.94 405.93">
<path fill="${colors.primary.paragraph}" d="M192.58 405.92a75.19 75.19 0 0 1-18.64-2.41l-1.2-.33-1.12-.56c-40.24-20.18-74.19-46.83-100.9-79.21a299.86 299.86 0 0 1-50.95-90.47A348.21 348.21 0 0 1 .07 110.27l.04-2.02c0-20.29 11.26-38.09 28.7-45.35C42.13 57.34 163.24 7.6 172 4c16.48-8.26 34.06-1.36 36.87-.16 6.31 2.58 118.28 48.38 142.47 59.9 24.94 11.87 31.6 33.2 31.6 43.93 0 48.6-8.43 94-25.02 134.97a312.52 312.52 0 0 1-56.16 90.51c-45.85 51.6-91.7 69.89-92.15 70.05a50.11 50.11 0 0 1-17.04 2.72zm-10.79-26.71c3.98.89 13.13 2.22 19.1.05 7.58-2.77 45.96-22.67 81.83-63.03 49.55-55.77 74.7-125.88 74.74-208.38-.1-1.67-1.28-13.59-17.07-21.1-23.72-11.3-140.1-58.89-141.27-59.37l-.32-.14c-2.44-1.02-10.2-3.17-15.55-.37l-1.08.5c-1.3.54-129.86 53.34-143.57 59.05-9.6 4-13 13.9-13 21.83 0 .58-.02 1.43-.05 2.52-1.1 56.44 11.97 195.34 156.24 268.44z"/>
<path fill="${colors.secondary.background}" d="M177.33 15.59S47.61 68.87 33.71 74.66c-13.9 5.79-20.85 19.7-20.85 33.6 0 13.9-10.45 195.26 164.47 282.96 0 0 15.88 4.39 27.92 0 12.04-4.39 164.96-78.52 164.96-283.55 0 0 0-20.85-24.33-32.43C321.55 63.66 203.94 15.6 203.94 15.6s-14.44-6.37-26.6 0z"/>
<path d="M191.23 57.29v284.25S60.34 278.53 61.51 112.89z" opacity=".2"/>
<path fill="${colors.primary.icon}" d="m192.94 261.58-41.69-53.61 24.24-18.86 19.75 25.38 66.7-70.4 22.3 21.13z"/>
</svg>`;
const MFASuccess = ({ recovery }: MFAStepProps) => {
const { colors } = useThemeColors();
return (
<View
style={{
alignItems: "center"
}}
>
<Seperator />
<SvgView width={150} height={150} src={mfaSvg(colors)} />
<Seperator />
<DialogHeader
centered={true}
title={
recovery
? strings.fallbackMethodEnabled()
: strings.twoFactorAuthEnabled()
}
paragraph={strings.accountIsSecure()}
padding={12}
/>
<Seperator />
<Button
title={strings.done()}
type="accent"
width={250}
onPress={() => {
eSendEvent(eCloseSheet);
}}
style={{
borderRadius: 100,
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
/>
{!recovery ? (
<Button
title={strings.secondary2faMethod()}
type="plain"
height={25}
onPress={() => {
MFASheet.present(true);
}}
/>
) : null}
</View>
);
};
export const MFASheet = ({ recovery }: { recovery?: boolean }) => {
const [step, setStep] = useState<MFAStep>({
id: "mfapick",
props: {
recovery: recovery
}
});
const steps: { [name: string]: JSX.Element } = {
mfapick: (
<MFAMethodsPickerStep
recovery={recovery}
onSuccess={(method) => {
setStep({
id: "setup",
props: {
method: method
}
});
}}
/>
),
setup: (
<MFASetup
recovery={recovery}
setStep={setStep}
{...step.props}
onSuccess={(method) => {
setStep({
id: "recoveryCodes",
props: {
method: method
}
});
}}
/>
),
recoveryCodes: (
<MFARecoveryCodes
recovery={recovery}
{...step.props}
onSuccess={(method) => {
setStep({
id: "success",
props: {
method: method
}
});
}}
/>
),
success: <MFASuccess {...step.props} recovery={recovery} />
};
return <View>{steps[step.id]}</View>;
};
MFASheet.present = (recovery?: boolean) => {
presentSheet({
component: <MFASheet recovery={recovery} />
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,118 @@
/*
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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Image, View, ViewStyle } from "react-native";
import { Radius, Spacing } from "../../../common/design/spacing";
import AppIcon from "../../../components/ui/AppIcon";
import Heading from "../../../components/ui/typography/heading";
import Paragraph from "../../../components/ui/typography/paragraph";
import { useUserStore } from "../../../stores/use-user-store";
import { getObfuscatedEmail } from "../../../utils/functions";
type AccountCardProps = {
style?: ViewStyle;
};
export const AccountCard = ({ style }: AccountCardProps) => {
const { colors } = useThemeColors();
const [user, profile] = useUserStore((state) => [state.user, state.profile]);
const fullName = profile?.fullName || strings.account();
const email = user?.email ? getObfuscatedEmail(user.email) : "";
return (
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={[
{
width: "100%",
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1,
padding: Spacing.LEVEL_2,
borderRadius: Radius.S,
backgroundColor: colors.primary.shade
},
style
]}
>
{profile?.profilePicture ? (
<Image
source={{
uri: profile.profilePicture
}}
style={{
width: 50,
height: 50,
borderRadius: Radius.XXL
}}
/>
) : (
<View
style={{
width: 50,
height: 50,
borderRadius: Radius.XXL,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="user"
iconFamily="notesnook"
size={18}
color={colors.secondary.icon}
/>
</View>
)}
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading fontSize="LG" fontFamily="SEMI_BOLD" lineHeight="100%">
{fullName}
</Heading>
{email ? (
<Paragraph
fontSize="SM"
lineHeight="100%"
color={colors.primary.paragraph}
>
{email}
</Paragraph>
) : null}
</View>
</View>
</View>
);
};
export default AccountCard;

View File

@@ -21,16 +21,16 @@ import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { IconButton } from "../../components/ui/icon-button";
import { ProgressBarComponent } from "../../components/ui/svg/lazy";
import Paragraph from "../../components/ui/typography/paragraph";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useDBItem } from "../../hooks/use-db-item";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { AppFontSize } from "../../utils/size";
import { db } from "../../../common/database";
import { IconButton } from "../../../components/ui/icon-button";
import { ProgressBarComponent } from "../../../components/ui/svg/lazy";
import Paragraph from "../../../components/ui/typography/paragraph";
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
import { useDBItem } from "../../../hooks/use-db-item";
import { useAttachmentStore } from "../../../stores/use-attachment-store";
import { AppFontSize } from "../../../utils/size";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import { DefaultAppStyles } from "../../../utils/styles";
export const AttachmentGroupProgress = (props: { groupId?: string }) => {
const { colors } = useThemeColors();

View File

@@ -19,30 +19,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { ReactElement } from "react";
import { View } from "react-native";
import { AttachmentDialog } from "../../components/attachments";
import { ChangePassword } from "../../components/auth/change-password";
import { DefaultAppStyles } from "../../utils/styles";
import { AttachmentDialog } from "../../../components/attachments";
import { ChangePassword } from "../../../components/auth/change-password";
import { DefaultAppStyles } from "../../../utils/styles";
import { AttachmentGroupProgress } from "./attachment-group-progress";
import { ChangeEmail } from "./change-email";
import DebugLogs from "./debug";
import { ConfigureToolbar } from "./editor/configure-toolbar";
import { Licenses } from "./licenses";
import {
ApplockTimerPicker,
BackupReminderPicker,
BackupWithAttachmentsReminderPicker,
DateFormatPicker,
DayFormatPicker,
WeekFormatPicker,
FontPicker,
HomePicker,
ImageCompressionPicker,
SidebarTabPicker,
TimeFormatPicker,
TrashIntervalPicker,
VaultLockTimerPicker
TimeFormatPicker
} from "./picker/pickers";
import { RestoreBackup } from "./restore-backup";
import { RestoreBackup } from "../restore-backup";
import { ServersConfiguration } from "./server-config";
import SoundPicker from "./sound-picker";
import ThemeSelector from "./theme-selector";
@@ -52,8 +48,9 @@ import {
ManageInboxKeys,
InboxKeysList,
SetupInboxKeys
} from "./manage-inbox-keys";
import { FailedInboxItems } from "./failed-inbox-items";
} from "../manage-inbox-keys";
import { FailedInboxItems } from "../failed-inbox-items";
import AccountCard from "./account-card";
export const components: { [name: string]: ReactElement } = {
homeselector: <HomePicker />,
@@ -62,17 +59,13 @@ export const components: { [name: string]: ReactElement } = {
"debug-logs": <DebugLogs />,
"sound-picker": <SoundPicker />,
licenses: <Licenses />,
"trash-interval-selector": <TrashIntervalPicker />,
"font-selector": <FontPicker />,
"title-format": <TitleFormat />,
"date-format-selector": <DateFormatPicker />,
"time-format-selector": <TimeFormatPicker />,
"day-format-selector": <DayFormatPicker />,
"week-format-selector": <WeekFormatPicker />,
"image-compression-picker": <ImageCompressionPicker />,
"theme-selector": <ThemeSelector />,
"applock-timer": <ApplockTimerPicker />,
"vault-lock-timer": <VaultLockTimerPicker />,
autobackupsattachments: <BackupWithAttachmentsReminderPicker />,
backuprestore: <RestoreBackup />,
"server-config": <ServersConfiguration />,
@@ -89,5 +82,6 @@ export const components: { [name: string]: ReactElement } = {
"manage-inbox-keys": <ManageInboxKeys />,
"inbox-keys": <InboxKeysList />,
"failed-inbox-items": <FailedInboxItems />,
"setup-inbox-keys": <SetupInboxKeys />
"setup-inbox-keys": <SetupInboxKeys />,
"account-card": <AccountCard />
};

View File

@@ -27,21 +27,22 @@ import React, { useEffect, useRef, useState } from "react";
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import filesystem from "../../common/filesystem";
import { presentDialog } from "../../components/dialog/functions";
import { IconButton } from "../../components/ui/icon-button";
import { Notice } from "../../components/ui/notice";
import Paragraph from "../../components/ui/typography/paragraph";
import useTimer from "../../hooks/use-timer";
import { ToastManager } from "../../services/event-manager";
import { hexToRGBA } from "../../utils/colors";
import { DefaultAppStyles } from "../../utils/styles";
import { Radius, Spacing } from "../../../common/design/spacing";
import filesystem from "../../../common/filesystem";
import { presentDialog } from "../../../components/dialog/functions";
import AppIcon from "../../../components/ui/AppIcon";
import { IconButton } from "../../../components/ui/icon-button";
import { Notice } from "../../../components/ui/notice";
import Heading from "../../../components/ui/typography/heading";
import Paragraph from "../../../components/ui/typography/paragraph";
import useTimer from "../../../hooks/use-timer";
import { ToastManager } from "../../../services/event-manager";
import { hexToRGBA } from "../../../utils/colors";
export default function DebugLogs() {
const { colors } = useThemeColors();
const { seconds, start } = useTimer("debug_logs_timer");
const listRef = useRef<FlatList>(null);
const currentOffset = useRef(0);
const [logs, setLogs] = useState<
{
key: string;
@@ -69,25 +70,27 @@ export default function DebugLogs() {
})();
}, [currentLog, seconds, start]);
const currentIndex = currentLog
? logs.findIndex((l) => l.key === currentLog.key)
: -1;
const renderItem = React.useCallback(
({ item }: { item: LogMessage; index: number }) => {
const background =
item.level === LogLevel.Error || item.level === LogLevel.Fatal
? hexToRGBA(colors.error.paragraph, 0.2)
: item.level === LogLevel.Warn
? hexToRGBA(colors.static.orange, 0.2)
: "transparent";
const isError =
item.level === LogLevel.Error || item.level === LogLevel.Fatal;
const isWarn = item.level === LogLevel.Warn;
const color =
item.level === LogLevel.Error || item.level === LogLevel.Fatal
? colors.error.paragraph
: item.level === LogLevel.Warn
const background = "transparent";
const color = isError
? colors.error.paragraph
: isWarn
? colors.static.black
: colors.primary.paragraph;
return !item ? null : (
<TouchableOpacity
activeOpacity={1}
activeOpacity={0.6}
onLongPress={() => {
Clipboard.setString(format(item));
ToastManager.show({
@@ -97,8 +100,7 @@ export default function DebugLogs() {
});
}}
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: Spacing.LEVEL_2,
backgroundColor: background,
flexShrink: 1,
borderBottomWidth: 1,
@@ -123,7 +125,6 @@ export default function DebugLogs() {
colors.primary.paragraph,
colors.error.paragraph,
colors.static.black,
colors.static.orange,
colors.primary.border
]
);
@@ -160,7 +161,10 @@ export default function DebugLogs() {
type: "success"
});
}
} catch (e) {}
} catch (e) {
/**
empty */
}
}, [currentLog?.logs]);
const copyLogs = React.useCallback(() => {
@@ -209,7 +213,8 @@ export default function DebugLogs() {
>
<View
style={{
padding: DefaultAppStyles.GAP
paddingHorizontal: Spacing.LEVEL_3,
paddingBottom: Spacing.LEVEL_4
}}
>
<Notice text={strings.debugNotice()} type="information" />
@@ -221,97 +226,143 @@ export default function DebugLogs() {
ListHeaderComponent={
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
flexDirection: "row",
alignItems: "center",
paddingBottom: Spacing.LEVEL_2,
gap: Spacing.LEVEL_2,
backgroundColor: colors.primary.background,
justifyContent: "space-between"
borderBottomWidth: 1,
borderBottomColor: colors.primary.border
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
alignItems: "center",
justifyContent: "space-between",
gap: Spacing.LEVEL_2
}}
>
<Paragraph>{currentLog.key}</Paragraph>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2,
flexShrink: 1
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name="file-text"
iconFamily="notesnook"
size={18}
color={colors.primary.icon}
/>
</View>
<IconButton
style={{
width: 30,
height: 30,
marginHorizontal: 5
}}
onPress={() => {
const index = logs.findIndex(
(l) => l.key === currentLog.key
);
if (index === 0) return;
setCurrentLog(logs[index - 1]);
}}
size={20}
name="chevron-left"
color={colors.primary.icon}
/>
<View
style={{
flexShrink: 1,
gap: Spacing.LEVEL_0
}}
>
<Heading fontSize="SM" lineHeight="100%" numberOfLines={1}>
{currentLog.key}
</Heading>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{`${currentIndex + 1} / ${logs.length}`}
</Paragraph>
</View>
</View>
<IconButton
<View
style={{
width: 30,
height: 30
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0
}}
onPress={() => {
const index = logs.findIndex(
(l) => l.key === currentLog.key
);
if (index === logs.length - 1) return;
setCurrentLog(logs[index + 1]);
}}
size={20}
name="chevron-right"
color={colors.primary.icon}
/>
</View>
>
<IconButton
onPress={() => {
if (currentIndex <= 0) return;
setCurrentLog(logs[currentIndex - 1]);
}}
disabled={currentIndex <= 0}
size={16}
name="chevron-right"
iconFamily="notesnook"
style={{
width: 36,
height: 36,
transform: [{ rotate: "180deg" }]
}}
color={
currentIndex <= 0
? colors.disabled.icon
: colors.primary.icon
}
/>
<IconButton
onPress={() => {
if (currentIndex === logs.length - 1) return;
setCurrentLog(logs[currentIndex + 1]);
}}
disabled={currentIndex === logs.length - 1}
size={16}
name="chevron-right"
iconFamily="notesnook"
style={{
width: 36,
height: 36
}}
color={
currentIndex === logs.length - 1
? colors.disabled.icon
: colors.primary.icon
}
/>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
onPress={copyLogs}
size={20}
style={{
width: 30,
height: 30,
marginRight: 5
}}
name="content-copy"
color={colors.secondary.paragraph}
/>
<IconButton
onPress={downloadLogs}
style={{
width: 30,
height: 30,
marginRight: 5
}}
size={20}
name="download"
color={colors.secondary.paragraph}
/>
<IconButton
onPress={clearLogs}
style={{
width: 30,
height: 30,
marginRight: 5
}}
size={20}
name="delete"
color={colors.secondary.paragraph}
/>
<IconButton
onPress={copyLogs}
size={16}
name="recovery-key-copy"
iconFamily="notesnook"
style={{
width: 36,
height: 36,
borderRadius: Radius.XS
}}
color={colors.primary.icon}
/>
<IconButton
onPress={downloadLogs}
size={16}
name="download-simple"
iconFamily="notesnook"
style={{
width: 36,
height: 36
}}
color={colors.primary.icon}
/>
<IconButton
onPress={clearLogs}
size={16}
name="trash"
iconFamily="notesnook"
style={{
width: 36,
height: 36
}}
color={colors.error.accent}
/>
</View>
</View>
</View>
}
@@ -320,6 +371,9 @@ export default function DebugLogs() {
width: "100%"
}}
stickyHeaderIndices={[0]}
contentContainerStyle={{
paddingHorizontal: Spacing.LEVEL_3
}}
ListFooterComponent={
<View
style={{

View File

@@ -24,13 +24,13 @@ import React from "react";
import { StyleSheet, View } from "react-native";
import { DraxProvider, DraxScrollView } from "react-native-drax";
import Animated, { FadeInDown, FadeOutDown } from "react-native-reanimated";
import PaywallSheet from "../../../components/sheets/paywall";
import { Button } from "../../../components/ui/button";
import { Notice } from "../../../components/ui/notice";
import Paragraph from "../../../components/ui/typography/paragraph";
import { ToastManager } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import PaywallSheet from "../../../../components/sheets/paywall";
import { Button } from "../../../../components/ui/button";
import { Notice } from "../../../../components/ui/notice";
import Paragraph from "../../../../components/ui/typography/paragraph";
import { ToastManager } from "../../../../services/event-manager";
import { AppFontSize } from "../../../../utils/size";
import { DefaultAppStyles } from "../../../../utils/styles";
import { Group } from "./group";
import { DragState, useDragState } from "./state";
export const ConfigureToolbar = () => {

View File

@@ -22,11 +22,11 @@ import * as React from "react";
import { View } from "react-native";
import { DraxDragWithReceiverEventData, DraxView } from "react-native-drax";
import Animated, { Layout } from "react-native-reanimated";
import { presentDialog } from "../../../components/dialog/functions";
import { IconButton } from "../../../components/ui/icon-button";
import Paragraph from "../../../components/ui/typography/paragraph";
import { getElevationStyle } from "../../../utils/elevation";
import { AppFontSize } from "../../../utils/size";
import { presentDialog } from "../../../../components/dialog/functions";
import { IconButton } from "../../../../components/ui/icon-button";
import Paragraph from "../../../../components/ui/typography/paragraph";
import { getElevationStyle } from "../../../../utils/elevation";
import { AppFontSize } from "../../../../utils/size";
import { renderTool } from "./common";
import { DraggableItem, useDragState } from "./state";
import ToolSheet from "./tool-sheet";
@@ -35,8 +35,8 @@ import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import type { ToolId } from "@notesnook/editor";
import { strings } from "@notesnook/intl";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { ToastManager } from "../../../services/event-manager";
import { DefaultAppStyles } from "../../../utils/styles";
import { ToastManager } from "../../../../services/event-manager";
import { DefaultAppStyles } from "../../../../utils/styles";
export const Group = ({
item,

View File

@@ -21,9 +21,9 @@ import { CURRENT_TOOLBAR_VERSION, migrateToolbar } from "@notesnook/common";
import type { ToolbarGroupDefinition } from "@notesnook/editor";
import { create } from "zustand";
import { StateStorage, persist } from "zustand/middleware";
import { db } from "../../../common/database";
import { MMKV } from "../../../common/database/mmkv";
import { useSettingStore } from "../../../stores/use-setting-store";
import { db } from "../../../../common/database";
import { MMKV } from "../../../../common/database/mmkv";
import { useSettingStore } from "../../../../stores/use-setting-store";
import { presets } from "./toolbar-definition";
export type ToolDefinition = string | string[];

View File

@@ -20,12 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import type { ToolId } from "@notesnook/editor";
import React, { RefObject } from "react";
import { View } from "react-native";
import { Pressable } from "../../../components/ui/pressable";
import { SvgView } from "../../../components/ui/svg";
import Paragraph from "../../../components/ui/typography/paragraph";
import { presentSheet } from "../../../services/event-manager";
import { Pressable } from "../../../../components/ui/pressable";
import { SvgView } from "../../../../components/ui/svg";
import Paragraph from "../../../../components/ui/typography/paragraph";
import { presentSheet } from "../../../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { defaultBorderRadius, AppFontSize } from "../../../../utils/size";
import { DraggableItem, useDragState } from "./state";
import {
findToolById,
@@ -34,7 +34,7 @@ import {
} from "./toolbar-definition";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import { DefaultAppStyles } from "../../../../utils/styles";
export default function ToolSheet({
group,
@@ -45,7 +45,7 @@ export default function ToolSheet({
}) {
const { colors } = useThemeColors();
const [data] = useDragState((state) => [state.data]);
const ungrouped = getUngroupedTools( data) as ToolId[];
const ungrouped = getUngroupedTools(data) as ToolId[];
const renderTool = React.useCallback(
(item: ToolId) => {

View File

@@ -22,12 +22,12 @@ import * as React from "react";
import { View } from "react-native";
import { DraxDragWithReceiverEventData, DraxView } from "react-native-drax";
import Animated, { Layout } from "react-native-reanimated";
import { presentDialog } from "../../../components/dialog/functions";
import { IconButton } from "../../../components/ui/icon-button";
import { SvgView } from "../../../components/ui/svg";
import Paragraph from "../../../components/ui/typography/paragraph";
import { getElevationStyle } from "../../../utils/elevation";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { presentDialog } from "../../../../components/dialog/functions";
import { IconButton } from "../../../../components/ui/icon-button";
import { SvgView } from "../../../../components/ui/svg";
import Paragraph from "../../../../components/ui/typography/paragraph";
import { getElevationStyle } from "../../../../utils/elevation";
import { AppFontSize, defaultBorderRadius } from "../../../../utils/size";
import { renderGroup } from "./common";
import { DraggableItem, useDragState } from "./state";
import ToolSheet from "./tool-sheet";
@@ -37,8 +37,8 @@ import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import type { ToolId } from "@notesnook/editor";
import { strings } from "@notesnook/intl";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { ToastManager } from "../../../services/event-manager";
import { DefaultAppStyles } from "../../../utils/styles";
import { ToastManager } from "../../../../services/event-manager";
import { DefaultAppStyles } from "../../../../utils/styles";
export const Tool = ({
item,

View File

@@ -32,7 +32,7 @@ export const presets: { [name: string]: ToolbarGroupDefinition[] } = {
custom: []
};
export function findToolById(id: keyof ReturnType<typeof tools> ): {
export function findToolById(id: keyof ReturnType<typeof tools>): {
title: string;
icon: string;
} {
@@ -51,7 +51,7 @@ export function getToolIcon(id: ToolId, color: string) {
export function getUngroupedTools(
toolDefinition: (string | string[])[][]
): string[] {
): string[] {
const allTools = tools();
const keys = Object.keys(allTools);

View File

@@ -20,11 +20,11 @@ import { LegendList } from "@legendapp/list";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Linking, Platform } from "react-native";
import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Pressable } from "../../../components/ui/pressable";
import Heading from "../../../components/ui/typography/heading";
import Paragraph from "../../../components/ui/typography/paragraph";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { LICENSES } from "./license-data";
type LicenseEntry = {

View File

@@ -0,0 +1,300 @@
/*
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 { CirclePartner, SubscriptionStatus } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useState } from "react";
import { useAsync } from "react-async-hook";
import { ActivityIndicator, Image, ScrollView, View } from "react-native";
import { db } from "../../../common/database";
import { Radius, Spacing } from "../../../common/design/spacing";
import { presentDialog } from "../../../components/dialog/functions";
import { Button } from "../../../components/ui/button";
import { Notice } from "../../../components/ui/notice";
import Heading from "../../../components/ui/typography/heading";
import Paragraph from "../../../components/ui/typography/paragraph";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import PremiumService from "../../../services/premium";
import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSimpleDialog } from "../../../utils/events";
import { openLinkInBrowser } from "../../../utils/functions";
import { AppFontSize } from "../../../utils/size";
import { sleep } from "../../../utils/time";
let p: any;
export const NotesnookCircle = () => {
const user = useUserStore((state) => state.user);
const isOnTrial =
PremiumService.get() &&
user?.subscription?.status === SubscriptionStatus.TRIAL;
const isFree = !PremiumService.get();
const partners = useAsync(db.circle.partners, [], {
initialState: () => p
});
p = partners.result;
useEffect(() => {
if (isFree || isOnTrial) {
presentDialog({
icon: "warning-circle",
centered: true,
iconFamily: "notesnook",
iconType: "error",
title: strings.subscriptionRequired(),
paragraph: isFree
? strings.freeUserCircleNotice()
: strings.trialUserCircleNotice(),
positiveText: isFree ? strings.upgradeNow() : strings.close(),
positivePress: async () => {
if (!isFree) {
Navigation.goBack();
return;
}
eSendEvent(eCloseSimpleDialog);
await sleep(300);
Navigation.navigate("PayWall", {
context: "logged-in"
});
return true;
},
onClose: () => {
Navigation.goBack();
}
});
}
}, []);
return (
<ScrollView
contentContainerStyle={{
gap: Spacing.LEVEL_3,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_0
}}
>
<View
style={{
gap: Spacing.LEVEL_0
}}
>
<Heading fontSize="XL">Partner offers</Heading>
<Paragraph>
Get exclusive discounts from our trusted partners who share our
commitment to privacy and user freedom.
</Paragraph>
</View>
{!isFree && !isOnTrial ? null : (
<View>
<Paragraph>
{isFree
? strings.freeUserCircleNotice()
: strings.trialUserCircleNotice()}
</Paragraph>
{isOnTrial ? null : (
<Button
title={strings.upgradePlan()}
onPress={() => {
Navigation.navigate("PayWall", {
canGoBack: true,
context: useUserStore.getState().user
? "logged-in"
: "logged-out"
});
}}
style={{
alignSelf: "flex-start",
paddingHorizontal: 0
}}
/>
)}
</View>
)}
{partners.loading ? <ActivityIndicator /> : null}
{partners.error ? (
<Notice type="alert" text={partners.error.message} />
) : null}
{partners.result?.map((item) => (
<Partner key={item.id} item={item} available={!isFree && !isOnTrial} />
))}
</ScrollView>
);
};
const Partner = ({
item,
available
}: {
item: CirclePartner;
available: boolean;
}) => {
const { colors } = useThemeColors();
const [code, setCode] = useState<string>();
return (
<View
style={{
borderRadius: Radius.MD,
backgroundColor: colors.secondary.background,
padding: Spacing.LEVEL_3,
gap: Spacing.LEVEL_1
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
<Image
src={item.logoBase64}
style={{
width: 24,
height: 24
}}
/>
<Heading>{item.name}</Heading>
</View>
<Paragraph fontSize="XS">{item.longDescription.trim()}</Paragraph>
<View
style={{
width: "100%",
borderRadius: Radius.XS,
backgroundColor: colors.primary.shade,
paddingVertical: 4,
paddingHorizontal: 8
}}
>
<Paragraph fontSize="XS" color={colors.primary.accent}>
{item.offerDescription}
</Paragraph>
</View>
{available ? (
<>
{!code ? (
<Button
type="tertiary"
style={{
borderWidth: 1,
borderColor: colors.primary.border
}}
title={strings.redeemCode()}
width="100%"
onPress={() => {
if (!PremiumService.get()) {
Navigation.navigate("PayWall", {
canGoBack: true,
context: useUserStore.getState().user
? "logged-in"
: "logged-out"
});
return;
}
db.circle
.redeem(item.id)
.then((result) => setCode(result?.code))
.catch((e) => ToastManager.error(e));
}}
/>
) : (
<>
<View
style={{
alignItems: "center",
flexDirection: "row",
gap: Spacing.LEVEL_2
}}
>
<View
style={{
backgroundColor: colors.tertiary.background,
borderRadius: Radius.XS,
paddingVertical: Spacing.LEVEL_1,
paddingHorizontal: Spacing.LEVEL_2,
flexGrow: 1
}}
>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
>
Discount code
</Paragraph>
<Paragraph
size={AppFontSize.xxs}
color={colors.primary.heading}
fontFamily="SEMI_BOLD"
>
{code}
</Paragraph>
</View>
<Button
title={strings.copy()}
type="accent"
style={{
paddingVertical: 0,
height: "100%"
}}
onPress={() => {
Clipboard.setString(code);
}}
/>
</View>
{item.codeRedeemUrl ? (
<Paragraph
fontSize="XS"
color={colors.primary.accent}
fontFamily="MEDIUM"
style={{
textDecorationLine: "underline"
}}
onPress={() => {
if (item.codeRedeemUrl) {
openLinkInBrowser(
item.codeRedeemUrl.replace("{{code}}", code)
);
}
}}
>
{strings.claimPromotion.clickHere()}{" "}
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{strings.claimPromotion.toClaim()}
</Paragraph>
</Paragraph>
) : null}
</>
)}
</>
) : null}
</View>
);
};

View File

@@ -0,0 +1,217 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { View } from "react-native";
import { Menu, MenuItem } from "react-native-material-menu";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { Dialog } from "../../../../components/dialog";
import { Pressable } from "../../../../components/ui/pressable";
import Paragraph from "../../../../components/ui/typography/paragraph";
import { getColorLinearShade } from "../../../../utils/colors";
import { AppFontSize, defaultBorderRadius } from "../../../../utils/size";
import { DefaultAppStyles } from "../../../../utils/styles";
import { verifyUser } from "../../verify-user";
import { Button } from "../../../../components/ui/button";
import { Radius, Spacing } from "../../../../common/design/spacing";
type PickerType = "menu" | "buttons";
interface PickerOptions<T, B = any> {
getValue: () => B;
updateValue: (item: T) => Promise<void>;
formatValue: (item: T) => any;
compareValue: (current: B, item: T) => boolean;
getItemKey: (item: T) => string;
options: T[];
isFeatureAvailable: () => Promise<boolean>;
isOptionAvailable: (item: T) => Promise<boolean>;
requiresVerification?: () => boolean;
onVerify?: () => Promise<boolean>;
pickerType?: PickerType;
}
export function SettingsPicker<T>({
getValue,
updateValue,
formatValue,
compareValue,
options,
getItemKey,
isFeatureAvailable,
isOptionAvailable,
requiresVerification = () => false,
onVerify,
pickerType = "buttons"
}: PickerOptions<T>) {
const { colors, isDark } = useThemeColors("contextMenu");
const menuRef = useRef<any>(null);
const [width, setWidth] = useState(0);
const [currentValue, setCurrentValue] = useState(getValue());
const onChange = async (item: T) => {
if ((await isFeatureAvailable()) && (await isOptionAvailable(item))) {
menuRef.current?.hide();
await updateValue(item);
setCurrentValue(item);
return;
}
menuRef.current?.hide();
await updateValue(item);
setCurrentValue(item);
};
return (
<View
onLayout={(event) => {
setWidth(event.nativeEvent.layout.width);
}}
style={{
width: "100%"
}}
>
{pickerType === "buttons" ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap",
gap: Spacing.LEVEL_1
}}
>
{options.map((item) => (
<Button
key={`option-${getItemKey(item)}`}
title={formatValue(item)}
fontSize={AppFontSize.sm}
fontFamily="REGULAR"
type={
compareValue(currentValue, item)
? "shade-plain"
: "secondary-outline"
}
onPress={() => {
onChange(item);
}}
style={{
paddingVertical: Spacing.LEVEL_1,
paddingHorizontal: Spacing.LEVEL_1,
borderRadius: Radius.XS
}}
/>
))}
</View>
) : null}
{pickerType === "menu" ? (
<Menu
ref={menuRef}
animationDuration={200}
style={{
borderRadius: defaultBorderRadius,
backgroundColor: colors.primary.background,
width: width,
marginTop: 60,
overflow: "hidden",
borderWidth: 0.7,
borderColor: getColorLinearShade(
colors.primary.background,
0.07,
isDark
)
}}
onRequestClose={() => {
menuRef.current?.hide();
}}
anchor={
<Pressable
onPress={async () => {
if (
(onVerify && !(await onVerify())) ||
!(await isFeatureAvailable())
)
return;
menuRef.current?.show();
}}
type="secondary"
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
justifyContent: "space-between",
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph>{formatValue(currentValue)}</Paragraph>
<Icon
color={colors.primary.icon}
name="menu-down"
size={AppFontSize.md}
/>
</Pressable>
}
>
<Dialog context="local" />
{options.map((item) => (
<MenuItem
key={getItemKey(item)}
onPress={async () => {
if (requiresVerification?.()) {
verifyUser("local", () => {
onChange(item);
});
} else {
onChange(item);
}
}}
pressColor={colors.primary.hover}
style={{
backgroundColor: compareValue(currentValue, item)
? colors.selected.background
: "transparent",
width: "100%",
maxWidth: width
}}
textStyle={{
fontSize: AppFontSize.sm,
fontFamily: "Inter-Regular",
color: compareValue(currentValue, item)
? colors.primary.accent
: colors.primary.paragraph
}}
>
{formatValue(item)}
</MenuItem>
))}
</Menu>
) : null}
</View>
);
}
export function createSettingsPicker<T, B>(props: PickerOptions<T, B>) {
const Selector = () => {
return <SettingsPicker {...props} />;
};
return Selector;
}

View File

@@ -18,26 +18,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
DATE_FORMATS,
DayFormat,
TIME_FORMATS,
TimeFormat,
TrashCleanupInterval,
WeekFormat
} from "@notesnook/core";
import { getFontById, getFonts } from "@notesnook/editor/dist/cjs/utils/font";
import dayjs from "dayjs";
import { createSettingsPicker } from ".";
import { db } from "../../../common/database";
import { ToastManager } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { Settings, useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { MenuItemsList } from "../../../utils/menu-items";
import { verifyUserWithApplock } from "../functions";
import { db } from "../../../../common/database";
import { ToastManager } from "../../../../services/event-manager";
import SettingsService from "../../../../services/settings";
import {
Settings,
useSettingStore
} from "../../../../stores/use-setting-store";
import { useUserStore } from "../../../../stores/use-user-store";
import { MenuItemsList } from "../../../../utils/menu-items";
import { strings } from "@notesnook/intl";
import { isFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../../../components/sheets/paywall";
import PaywallSheet from "../../../../components/sheets/paywall";
const DAY_FORMATS = ["short", "long"];
const DayFormatFormats = {
@@ -128,56 +128,6 @@ export const SidebarTabPicker = createSettingsPicker({
isOptionAvailable: async () => true
});
export const TrashIntervalPicker = createSettingsPicker({
getValue: () => db.settings.getTrashCleanupInterval(),
updateValue: async (item) => {
db.settings.setTrashCleanupInterval(item);
},
formatValue: (item) => {
return item === -1
? strings.never()
: item === 1
? strings.reminderRecurringMode.day()
: strings.days(item);
},
getItemKey: (item) => item.toString(),
options: [-1, 1, 7, 30, 365] as TrashCleanupInterval[],
compareValue: (current, item) => current === item,
isFeatureAvailable: async () => true,
isOptionAvailable: async () => {
const disableTrashFeature = await isFeatureAvailable("disableTrashCleanup");
if (!disableTrashFeature.isAllowed) {
ToastManager.show({
message: disableTrashFeature.error,
type: "info",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(disableTrashFeature);
}
});
}
return disableTrashFeature.isAllowed;
}
});
export const DateFormatPicker = createSettingsPicker({
getValue: () => db.settings.getDateFormat(),
updateValue: async (item) => {
db.settings.setDateFormat(item);
useSettingStore.setState({
dateFormat: item
});
},
formatValue: (item) => {
return `${item} (${dayjs().format(item)})`;
},
getItemKey: (item) => item,
options: DATE_FORMATS,
compareValue: (current, item) => current === item,
isFeatureAvailable: async () => true,
isOptionAvailable: async () => true
});
export const DayFormatPicker = createSettingsPicker({
getValue: () => db.settings.getDayFormat(),
updateValue: async (item) => {
@@ -249,7 +199,7 @@ export const BackupReminderPicker = createSettingsPicker<
return item === "useroff" ? strings.off() : strings[item]?.();
},
getItemKey: (item) => item,
options: ["useroff", "daily", "weekly", "monthly"],
options: ["daily", "weekly", "monthly", "useroff"],
compareValue: (current, item) => current === item,
requiresVerification: () => {
return (
@@ -273,7 +223,7 @@ export const BackupWithAttachmentsReminderPicker = createSettingsPicker({
: item.slice(0, 1).toUpperCase() + item.slice(1);
},
getItemKey: (item) => item,
options: ["never", "weekly", "monthly"] as Settings["fullBackupReminder"][],
options: ["weekly", "monthly", "never"] as Settings["fullBackupReminder"][],
compareValue: (current, item) => current === item,
requiresVerification: () => {
return (
@@ -285,61 +235,6 @@ export const BackupWithAttachmentsReminderPicker = createSettingsPicker({
isOptionAvailable: async () => true
});
export const ApplockTimerPicker = createSettingsPicker({
getValue: () => useSettingStore.getState().settings.appLockTimer,
updateValue: async (item) => {
SettingsService.set({ appLockTimer: item });
},
formatValue: (item) => {
return item === -1
? strings.never()
: item === 0 || item === undefined
? strings.immediately()
: item === 1
? strings.minutes(1)
: strings.minutes(item);
},
getItemKey: (item) => item.toString(),
options: [-1, 0, 1, 5, 15, 30],
compareValue: (current, item) => current === item,
onVerify: () => {
return verifyUserWithApplock();
},
isFeatureAvailable: async () => true,
isOptionAvailable: async () => true
});
export const VaultLockTimerPicker = createSettingsPicker({
getValue: () => useSettingStore.getState().vaultLockAfter,
updateValue: async (item) => {
await db.settings.setVaultLockAfter(item);
useSettingStore.setState({
vaultLockAfter: item
});
},
formatValue: (item) => {
return item === -1
? strings.never()
: item < 1000 * 60 * 60
? strings.minutes(item / (1000 * 60))
: strings.hours(item / (1000 * 60 * 60));
},
getItemKey: (item) => item.toString(),
options: [
1000 * 60 * 1,
1000 * 60 * 5,
1000 * 60 * 10,
1000 * 60 * 15,
1000 * 60 * 30,
1000 * 60 * 45,
1000 * 60 * 60,
-1
],
compareValue: (current, item) => current === item,
isFeatureAvailable: async () => true,
isOptionAvailable: async () => true
});
export const ImageCompressionPicker = createSettingsPicker({
getValue: () => useSettingStore.getState().settings.imageCompression,
updateValue: async (item) => {
@@ -347,9 +242,9 @@ export const ImageCompressionPicker = createSettingsPicker({
},
formatValue: (item) => {
return item === "ask-every-time"
? strings.askEveryTime()
? strings.alwaysAsk()
: item === "enabled"
? strings.enableRecommended()
? strings.enable()
: strings.disable();
},
getItemKey: (item) => item,

View File

@@ -0,0 +1,247 @@
/*
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 { isServerCompatible } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { View } from "react-native";
import { presentDialog } from "../../../components/dialog/functions";
import { Button } from "../../../components/ui/button";
import Input from "../../../components/ui/input";
import { Notice } from "../../../components/ui/notice";
import Paragraph from "../../../components/ui/typography/paragraph";
import { ToastManager } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { HostId, HostIds } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { DefaultAppStyles } from "../../../utils/styles";
import { Spacing } from "../../../common/design/spacing";
export const ServerIds = [
"notesnook-sync",
"auth",
"sse",
"monograph"
] as const;
export type ServerId = (typeof ServerIds)[number];
type Server = {
id: ServerId;
host: HostId;
title: string;
example: string;
description: string;
versionEndpoint: string;
};
type VersionResponse = {
version: number;
id: string;
instance: string;
};
const SERVERS: Server[] = [
{
id: "notesnook-sync",
host: "API_HOST",
title: strings.syncServer(),
example: "http://localhost:4326",
description: strings.syncServerDesc(),
versionEndpoint: "/version"
},
{
id: "auth",
host: "AUTH_HOST",
title: strings.authServer(),
example: "http://localhost:5326",
description: strings.authServerDesc(),
versionEndpoint: "/version"
},
{
id: "sse",
host: "SSE_HOST",
title: strings.sseServer(),
example: "http://localhost:7326",
description: strings.sseServerDesc(),
versionEndpoint: "/version"
},
{
id: "monograph",
host: "MONOGRAPH_HOST",
title: strings.monographServer(),
example: "http://localhost:6326",
description: strings.monographServerDesc(),
versionEndpoint: "/api/version"
}
];
export function ServersConfiguration() {
const { colors } = useThemeColors();
const [error, setError] = useState<string>();
const [success, setSuccess] = useState<boolean>();
const [urls, setUrls] = useState<Partial<Record<HostId, string>>>(
SettingsService.getProperty("serverUrls") || {}
);
const isLoggedIn = useUserStore((state) => !!state.user);
return (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
gap: Spacing.LEVEL_2
}}
>
{isLoggedIn ? (
<Notice text={strings.logoutToChangeServerUrls()} type="information" />
) : null}
{!isLoggedIn ? (
<View style={{ flexDirection: "column", gap: Spacing.LEVEL_2 }}>
{SERVERS.map((server) => (
<Input
key={server.id}
editable={!isLoggedIn}
containerStyle={{
borderWidth: 0,
backgroundColor: colors.secondary.background
}}
placeholder={`${server.id} e.g. ${server.example}`}
validationType="url"
defaultValue={urls[server.host]}
errorMessage={strings.enterValidUrl()}
onChangeText={(value) =>
setUrls((s) => {
s[server.host] = value;
return s;
})
}
/>
))}
{error ? (
<Paragraph
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
color={colors.error.paragraph}
>
{error}
</Paragraph>
) : null}
{success === true ? (
<Paragraph
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
color={colors.success.paragraph}
>
{strings.connectedToServer()}
</Paragraph>
) : null}
<View
style={{
marginTop: 1,
justifyContent: "flex-end",
gap: Spacing.LEVEL_2
}}
>
<Button
disabled={isLoggedIn}
type="secondary"
width="100%"
onPress={async () => {
setError(undefined);
try {
for (const host of HostIds) {
const url = urls[host];
const server = SERVERS.find((s) => s.host === host)!;
if (!server) throw new Error(strings.serverNotFound(host));
if (!url) throw new Error(strings.allServerUrlsRequired());
const version = await fetch(
`${url}${server.versionEndpoint}`
)
.then((r) => r.json() as Promise<VersionResponse>)
.catch(() => undefined);
if (!version)
throw new Error(
`${strings.couldNotConnectTo(server.title)}`
);
if (version.id !== server.id)
throw new Error(
`${strings.incorrectServerUrl(url, server.title)}.`
);
if (!isServerCompatible(version.version)) {
throw new Error(
strings.serverVersionMismatch(server.title, url)
);
}
}
setSuccess(true);
} catch (e) {
setError((e as Error).message);
}
}}
title={strings.testConnection()}
/>
<Button
type="accent"
disabled={isLoggedIn}
width="100%"
onPress={async () => {
if (!success) {
ToastManager.show({
heading: strings.testConnectionBeforeSave()
});
return;
}
SettingsService.setProperty(
"serverUrls",
urls as Record<HostId, string>
);
presentDialog({
title: strings.serverUrlChanged(),
paragraph: strings.restartAppToTakeEffect(),
negativeText: strings.done()
});
}}
title={strings.save()}
/>
<Button
disabled={isLoggedIn}
type="error"
width="100%"
title={strings.resetServerUrls()}
onPress={async () => {
if (isLoggedIn) return;
SettingsService.setProperty("serverUrls", undefined);
presentDialog({
title: strings.serverUrlsReset(),
paragraph: strings.restartAppToTakeEffect(),
negativeText: strings.done()
});
}}
/>
</View>
</View>
) : null}
</View>
);
}

View File

@@ -24,16 +24,16 @@ import NotificationSounds, {
stopSampleSound
} from "react-native-notification-sounds";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { IconButton } from "../../components/ui/icon-button";
import { Pressable } from "../../components/ui/pressable";
import Paragraph from "../../components/ui/typography/paragraph";
import Notifications from "../../services/notifications";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
import { IconButton } from "../../../components/ui/icon-button";
import { Pressable } from "../../../components/ui/pressable";
import Paragraph from "../../../components/ui/typography/paragraph";
import Notifications from "../../../services/notifications";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { AppFontSize } from "../../../utils/size";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import { DefaultAppStyles } from "../../../utils/styles";
const SoundItem = ({
playingSoundId,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import { strings } from "@notesnook/intl";
import React, { useState } from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import { Radius, Spacing } from "../../../common/design/spacing";
import { Button } from "../../../components/ui/button";
import Input from "../../../components/ui/input";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
const TITLE_FORMAT_TOKENS: { token: string; title: () => string }[] = [
{ token: "$date$", title: strings.titleFormatCurrentDate },
{ token: "$time$", title: strings.titleFormatCurrentTime },
{ token: "$day$", title: strings.titleFormatCurrentDay },
{ token: "$timestamp$", title: strings.titleFormatTimestamp },
{ token: "$count$", title: strings.titleFormatNoteCount },
{ token: "$headline$", title: strings.titleFormatHeadline }
];
export const TitleFormat = () => {
const [titleFormat, setTitleFormat] = useState(
db.settings.getTitleFormat() || ""
);
const { colors } = useThemeColors();
const updateFormat = (value: string) => {
setTitleFormat(value);
db.settings.setTitleFormat(value);
};
return (
<View
style={{
width: "100%",
gap: Spacing.LEVEL_2
}}
>
<Input
value={titleFormat}
onChangeText={updateFormat}
onSubmit={(e) => updateFormat(e.nativeEvent.text)}
containerStyle={{
borderWidth: 0,
backgroundColor: colors.secondary.background
}}
inputStyle={{
color: colors.primary.heading
}}
/>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: DefaultAppStyles.GAP_SMALL
}}
>
{TITLE_FORMAT_TOKENS.map((item) => (
<Button
key={item.token}
title={item.title()}
type="secondary"
bold={false}
fontSize={AppFontSize.xs}
onPress={() => updateFormat(`${titleFormat}${item.token}`)}
fontFamily="REGULAR"
style={{
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_1,
borderRadius: Radius.XS,
width: undefined,
borderWidth: 0
}}
/>
))}
</View>
</View>
);
};

View File

@@ -0,0 +1,316 @@
/*
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 { formatBytes, getFormattedDate } from "@notesnook/common";
import {
SubscriptionPlan,
SubscriptionProvider,
SubscriptionStatus
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React from "react";
import { Platform, TouchableOpacity, View } from "react-native";
import { Radius, Spacing } from "../../../common/design/spacing";
import { PlanLimits } from "../../../components/sheets/plan-limits";
import { Button } from "../../../components/ui/button";
import Heading from "../../../components/ui/typography/heading";
import Paragraph from "../../../components/ui/typography/paragraph";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { useUserStore } from "../../../stores/use-user-store";
import { planToDisplayName } from "../../../utils/constants";
import { AppFontSize } from "../../../utils/size";
import { SectionItem } from "../section-item";
export const getTimeLeft = (t2) => {
let daysRemaining = dayjs(t2).diff(dayjs(), "days");
return {
time: dayjs(t2).diff(dayjs(), daysRemaining === 0 ? "hours" : "days"),
isHour: daysRemaining === 0
};
};
const getBillingSubtitle = (user) => {
if (!user?.subscription || user.subscription.plan === SubscriptionPlan.FREE) {
return null;
}
const isAnnual = user.subscription.productId?.includes("yearly");
const billingLabel =
user?.subscription?.provider === SubscriptionProvider.STREETWRITERS
? "Awarded"
: isAnnual
? "Billed annually"
: user.subscription.productId?.includes("monthly")
? "Billed monthly"
: null;
if (!user.subscription.expiry) return billingLabel;
const expiryLabel = getFormattedDate(user.subscription.expiry, "date");
const renewalLabel =
user.subscription.status === SubscriptionStatus.CANCELED ||
user.subscription.status === SubscriptionStatus.PAUSED ||
user.subscription.status === SubscriptionStatus.EXPIRED ||
user?.subscription?.provider === SubscriptionProvider.STREETWRITERS
? "Ends"
: "Renews";
return [billingLabel, `${renewalLabel} ${expiryLabel}`]
.filter(Boolean)
.join(" • ");
};
const SettingsUserSection = ({ item }) => {
const { colors } = useThemeColors();
const [user] = useUserStore((state) => [state.user]);
const used = user?.storageUsed || 0;
const total = user?.totalStorage || 0;
const storagePercent = total > 0 ? Math.min((used / total) * 100, 100) : 0;
const planName = planToDisplayName(user?.subscription?.plan);
const billingSubtitle = getBillingSubtitle(user);
const showUpgradeButton = !(
((user?.subscription?.provider === SubscriptionProvider.PADDLE ||
user?.subscription?.provider === SubscriptionProvider.STREETWRITERS ||
!(
(user?.subscription?.provider === SubscriptionProvider.APPLE &&
Platform.OS === "ios") ||
(user?.subscription?.provider === SubscriptionProvider.GOOGLE &&
Platform.OS === "android")
)) &&
PremiumService.get()) ||
SettingsService.getProperty("serverUrls")
);
const storageText =
total === -1
? `${formatBytes(used)}/Unlimited ${strings.used()}`
: `${formatBytes(used)}/${formatBytes(total)} ${strings.used()}`;
return (
<>
{user ? (
<>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
marginBottom: Spacing.LEVEL_2,
marginTop: -Spacing.LEVEL_2
}}
>
<View
style={{
backgroundColor: colors.secondary.background,
borderRadius: Radius.S,
padding: Spacing.LEVEL_2,
gap: Spacing.LEVEL_3,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: Spacing.LEVEL_2,
width: "100%"
}}
>
<View
style={{
flexShrink: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading
fontSize="XL"
fontFamily="SEMI_BOLD"
lineHeight={null}
color={colors.primary.heading}
>
{planName}
</Heading>
{billingSubtitle ? (
<Paragraph fontSize="XS" color={colors.primary.paragraph}>
{billingSubtitle}
</Paragraph>
) : null}
</View>
<Button
title={strings.upgrade().toUpperCase()}
type="accent"
disabled={!showUpgradeButton}
fontSize={AppFontSize.sm}
style={{
alignSelf: "flex-start",
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_2,
borderRadius: Radius.XS
}}
textStyle={{
textTransform: "uppercase"
}}
onPress={() => {
if (
user?.subscription?.plan === SubscriptionPlan.LEGACY_PRO
) {
ToastManager.show({
message: strings.cannotChangePlan(),
context: "local"
});
return;
}
if (
user.subscription?.plan !== SubscriptionPlan.FREE &&
user.subscription?.productId &&
user.subscription?.productId.includes("5year")
) {
ToastManager.show({
message:
"You have made a one time purchase. To change your plan please contact support.",
type: "info"
});
return;
}
Navigation.navigate("PayWall", {
context: "logged-in",
canGoBack: true
});
}}
/>
</View>
<View
style={{
gap: Spacing.LEVEL_1,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: Spacing.LEVEL_2,
width: "100%"
}}
>
<Paragraph
fontSize="XS"
fontFamily="MEDIUM"
color={colors.secondary.paragraph}
>
{strings.storage()}
</Paragraph>
<Paragraph
fontSize="XS"
fontFamily="MEDIUM"
color={colors.secondary.paragraph}
>
{storageText}
</Paragraph>
</View>
<View
style={{
backgroundColor: colors.tertiary.background,
width: "100%",
height: 8,
borderRadius: Radius.XXL,
overflow: "hidden"
}}
>
<View
style={{
backgroundColor: colors.primary.accent,
height: 8,
width: `${storagePercent}%`,
borderRadius: Radius.XXL
}}
/>
</View>
<TouchableOpacity
activeOpacity={0.8}
onPress={() => {
presentSheet({
component: <PlanLimits />
});
}}
style={{
alignSelf: "flex-start"
}}
>
<Paragraph
fontSize="XS"
fontFamily="SEMI_BOLD"
color={colors.primary.accent}
>
{strings.viewAllLimits()}
</Paragraph>
</TouchableOpacity>
</View>
</View>
</View>
<Heading
style={{
paddingHorizontal: Spacing.LEVEL_3,
marginBottom: Spacing.LEVEL_2
}}
color={colors.secondary.paragraph}
size={AppFontSize.sm}
fontFamily="MEDIUM"
>
{strings.account()}
</Heading>
{item.sections.map((item) => (
<SectionItem key={item.name} item={item} />
))}
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
width: "100%"
}}
>
<View
style={{
height: 1,
width: "100%",
backgroundColor: colors.primary.separator
}}
/>
</View>
</>
) : null}
</>
);
};
export default SettingsUserSection;

View File

@@ -23,15 +23,17 @@ import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { FontSizes } from "../../common/design/font";
import { Radius, Spacing } from "../../common/design/spacing";
import { db } from "../../common/database";
import { presentDialog } from "../../components/dialog/functions";
import { Header } from "../../components/header";
import AppIcon from "../../components/ui/AppIcon";
import { Button } from "../../components/ui/button";
import { IconButton } from "../../components/ui/icon-button";
import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { ToastManager } from "../../services/event-manager";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Header } from "../../components/header";
type FailedInboxItem = {
id: string;
@@ -51,50 +53,7 @@ function parseErrorContext(
}
}
function ErrorBadge({
message
}: {
message: InboxItemsHistoryErrorContext["message"] | undefined;
}) {
const { colors } = useThemeColors();
if (!message) {
return (
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
N/A
</Paragraph>
);
}
const palette =
message === "Invalid JSON"
? { background: "rgba(255, 152, 0, 0.15)", paragraph: "#e65100" }
: message === "Validation failed"
? { background: "rgba(255, 193, 7, 0.15)", paragraph: "#8a6000" }
: colors.error;
return (
<View
style={{
alignSelf: "flex-start",
backgroundColor: palette.background,
borderRadius: defaultBorderRadius,
paddingVertical: 4,
paddingHorizontal: 8
}}
>
<Paragraph
size={AppFontSize.xxs}
color={palette.paragraph}
style={{ fontWeight: "700" }}
>
{message}
</Paragraph>
</View>
);
}
function DetailsBlock({ value }: { value: string }) {
function PayloadDataBlock({ value }: { value: string }) {
const { colors } = useThemeColors();
const [copied, setCopied] = useState(false);
@@ -107,25 +66,27 @@ function DetailsBlock({ value }: { value: string }) {
return (
<View
style={{
borderWidth: 1,
borderColor: colors.secondary.border,
borderRadius: defaultBorderRadius,
backgroundColor: colors.secondary.background,
overflow: "hidden"
borderRadius: Radius.S,
padding: Spacing.LEVEL_2,
gap: Spacing.LEVEL_2,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "flex-end",
borderBottomWidth: 1,
borderBottomColor: colors.secondary.border
alignItems: "center",
justifyContent: "space-between",
width: "100%"
}}
>
<IconButton
name={copied ? "check" : "content-copy"}
color={colors.primary.icon}
size={AppFontSize.lg}
<Heading fontSize="MD" lineHeight="100%">
{strings.payloadData()}
</Heading>
<Pressable
type="transparent"
onPress={() => {
try {
Clipboard.setString(value);
@@ -141,24 +102,56 @@ function DetailsBlock({ value }: { value: string }) {
});
}
}}
/>
</View>
<ScrollView
style={{ maxHeight: 120 }}
contentContainerStyle={{
padding: DefaultAppStyles.GAP_SMALL
}}
>
<Paragraph
size={AppFontSize.xxs}
style={{
fontFamily: "monospace"
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1,
paddingVertical: 0,
paddingHorizontal: 0,
flexShrink: 1,
width: undefined
}}
>
{value}
</Paragraph>
</ScrollView>
<AppIcon
name={copied ? "check" : "copy"}
iconFamily={"notesnook"}
size={16}
color={colors.primary.accent}
/>
<Paragraph
fontFamily="MEDIUM"
fontSize="SM"
color={colors.primary.accent}
>
{strings.copy()}
</Paragraph>
</Pressable>
</View>
<View style={{ height: 1, backgroundColor: colors.primary.border }} />
<View
style={{
backgroundColor: colors.tertiary.background,
borderRadius: Radius.S,
padding: Spacing.LEVEL_2,
width: "100%"
}}
>
<ScrollView
style={{ maxHeight: 160, width: "100%" }}
nestedScrollEnabled
>
<Paragraph
fontSize="SM"
color={colors.primary.paragraph}
style={{ fontFamily: "monospace" }}
>
{value}
</Paragraph>
</ScrollView>
</View>
</View>
);
}
@@ -197,7 +190,9 @@ export const FailedInboxItems = () => {
rightButton={
items?.length > 0
? {
name: "delete",
name: "trash",
iconFamily: "notesnook",
size: 20,
color: colors.primary.icon,
onPress: async () => {
presentDialog({
@@ -226,7 +221,7 @@ export const FailedInboxItems = () => {
flex: 1,
justifyContent: "center",
alignItems: "center",
gap: DefaultAppStyles.GAP_VERTICAL
gap: Spacing.LEVEL_2
}}
>
<ActivityIndicator size="small" color={colors.primary.accent} />
@@ -242,8 +237,8 @@ export const FailedInboxItems = () => {
flex: 1,
justifyContent: "center",
alignItems: "center",
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP
gap: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_3
}}
>
<Paragraph color={colors.error.paragraph}>
@@ -260,32 +255,25 @@ export const FailedInboxItems = () => {
{items.length === 0 ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingTop: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_2,
flex: 1,
justifyContent: "center"
justifyContent: "center",
alignItems: "center"
}}
>
<View
style={{
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius,
alignItems: "center"
}}
>
<Paragraph color={colors.secondary.paragraph}>
{strings.noFailedInboxItems()}
</Paragraph>
</View>
<Paragraph color={colors.secondary.paragraph}>
{strings.noFailedInboxItems()}
</Paragraph>
</View>
) : null}
{result.status === "fulfilled" && items.length > 0 ? (
<ScrollView
contentContainerStyle={{
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
gap: Spacing.LEVEL_4,
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_4,
paddingBottom: 50
}}
>
@@ -304,96 +292,78 @@ export const FailedInboxItems = () => {
<View
key={item.id}
style={{
borderWidth: 1,
borderColor: colors.secondary.border,
borderRadius: defaultBorderRadius,
backgroundColor: colors.primary.background,
padding: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
gap: Spacing.LEVEL_3,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between"
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{getFormattedDate(item.dateSynced, "date-time")}
</Paragraph>
<View style={{ gap: Spacing.LEVEL_2, width: "100%" }}>
<View style={{ gap: Spacing.LEVEL_1, width: "100%" }}>
<Heading fontSize="LG" lineHeight="100%">
{(message as string) || strings.failed()}
</Heading>
{description ? (
<Paragraph fontSize="SM" color={colors.primary.paragraph}>
{description as string}
</Paragraph>
) : null}
</View>
<ErrorBadge message={message} />
</View>
<View
style={{
flexDirection: "row",
gap: DefaultAppStyles.GAP_SMALL,
alignItems: "flex-start"
}}
>
<View
style={{
backgroundColor: colors.error.background,
padding: 3,
paddingHorizontal: 6,
borderRadius: 4,
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap"
gap: Spacing.LEVEL_0 + 2
}}
>
<Paragraph color={colors.error.paragraph}>Error</Paragraph>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{getFormattedDate(item.dateSynced, "date")}
</Paragraph>
<View
style={{
width: 4,
height: 4,
borderRadius: Radius.MD,
backgroundColor: colors.secondary.paragraph
}}
/>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{getFormattedDate(item.dateSynced, "time")}
</Paragraph>
</View>
<Paragraph
style={{
flexShrink: 1
}}
>
{(description as string) || "N/A"}
</Paragraph>
</View>
{details ? (
<DetailsBlock value={details} />
) : (
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
N/A
</Paragraph>
)}
{details ? <PayloadDataBlock value={details} /> : null}
<View style={{ alignItems: "flex-end" }}>
<Button
title={strings.delete()}
type="error"
style={{
width: "100%"
}}
onPress={() => {
presentDialog({
title: strings.delete(),
paragraph: strings.areYouSure(),
positiveText: strings.delete(),
positiveType: "error",
negativeText: strings.cancel(),
positivePress: async () => {
try {
await deleteItem(item.id);
return true;
} catch (error) {
ToastManager.error(error as Error);
return false;
}
<Button
title={strings.delete()}
type="error"
fontSize={FontSizes.MD}
style={{
width: "100%",
borderWidth: 1,
borderColor: colors.error.border,
borderRadius: Radius.XS,
paddingVertical: Spacing.LEVEL_3
}}
onPress={() => {
presentDialog({
title: strings.delete(),
paragraph: strings.areYouSure(),
positiveText: strings.delete(),
positiveType: "error",
negativeText: strings.cancel(),
positivePress: async () => {
try {
await deleteItem(item.id);
return true;
} catch (error) {
ToastManager.error(error as Error);
return false;
}
});
}}
/>
</View>
}
});
}}
/>
</View>
);
})}

View File

@@ -1,159 +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 { strings } from "@notesnook/intl";
import { db } from "../../common/database";
import { validateAppLockPassword } from "../../common/database/encryption";
import { presentDialog } from "../../components/dialog/functions";
import BiometricService from "../../services/biometrics";
import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { sleep } from "../../utils/time";
export async function verifyUser(
context,
onsuccess,
disableBackdropClosing,
onclose,
closeText
) {
presentDialog({
context: context,
title: strings.verifyItsYou(),
input: true,
inputPlaceholder: strings.enterPassword(),
paragraph: strings.enterPasswordDesc(),
positiveText: strings.verify(),
secureTextEntry: true,
disableBackdropClosing: disableBackdropClosing,
onClose: onclose,
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) {
sleep(300).then(async () => {
await onsuccess();
});
} else {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "global"
});
return false;
}
} catch (e) {
ToastManager.show({
heading: strings.verifyFailed(),
message: e.message,
type: "error",
context: "global"
});
return false;
}
}
});
}
export async function verifyUserWithApplock() {
const keyboardType = SettingsService.getProperty("applockKeyboardType");
return new Promise((resolve) => {
if (SettingsService.getProperty("appLockHasPasswordSecurity")) {
presentDialog({
title: strings.verifyItsYou(),
input: true,
inputPlaceholder:
keyboardType == "numeric"
? strings.enterApplockPin()
: strings.enterApplockPassword(),
paragraph:
keyboardType == "numeric"
? strings.enterApplockPinDesc()
: strings.enterApplockPasswordDesc(),
positiveText: strings.verify(),
secureTextEntry: true,
negativeText: strings.cancel(),
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({
heading: strings.invalid(
keyboardType === "numeric" ? "pin" : "password"
),
type: "error",
context: "local"
});
return false;
}
resolve(verified);
} catch (e) {
resolve(false);
return false;
}
return true;
}
});
} else {
BiometricService.isBiometryAvailable().then((available) => {
if (available) {
BiometricService.validateUser(strings.verifyItsYou()).then(
(verified) => {
resolve(verified);
}
);
} else if (useUserStore.getState().user) {
let verified = false;
verifyUser(
null,
() => {
resolve(true);
},
false,
() => {
resolve(verified);
}
);
} else {
resolve(true);
}
});
}
});
}

View File

@@ -26,10 +26,12 @@ import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import useNavigationStore from "../../stores/use-navigation-store";
import { components } from "./components";
import { components } from "./components/components";
import { SectionItem } from "./section-item";
import { RouteParams, SettingSection } from "./types";
import { useThemeColors } from "@notesnook/theme";
import { SectionGroup } from "./section-group";
import { Spacing } from "../../common/design/spacing";
const keyExtractor = (item: SettingSection) => item.id;
const AnimatedKeyboardAvoidingFlatList = Animated.createAnimatedComponent(
KeyboardAwareFlatList
@@ -39,15 +41,25 @@ const Group = ({
navigation,
route
}: NativeStackScreenProps<RouteParams, "SettingsGroup">) => {
const { colors } = useThemeColors();
useNavigationFocus(navigation, {
onFocus: () => {
useNavigationStore.getState().setFocusedRouteId("Settings");
return false;
}
});
const renderItem = ({ item }: { item: SettingSection; index: number }) => (
<SectionItem item={item} />
);
const renderItem = ({
item,
index
}: {
item: SettingSection;
index: number;
}) =>
item.type === "group" ? (
<SectionGroup item={item} isLast={!route.params.sections?.[index + 1]} />
) : (
<SectionItem item={item} />
);
return (
<>
@@ -56,6 +68,12 @@ const Group = ({
renderedInRoute="Settings"
title={route.params.name as string}
canGoBack={true}
style={{
backgroundColor: "transparent",
borderRadius: 0,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border
}}
id="Settings"
/>
)}
@@ -65,10 +83,21 @@ const Group = ({
flex: 1
}}
>
{route.params.component ? components[route.params.component] : null}
{!route.params.sections && route.params.component
? components[route.params.component]
: null}
{route.params.sections ? (
<AnimatedKeyboardAvoidingFlatList
data={route.params.sections}
ListHeaderComponent={
route.params.component
? components[route.params.component]
: null
}
contentContainerStyle={{
gap: Spacing.LEVEL_2
}}
keyExtractor={keyExtractor}
renderItem={renderItem}
enableOnAndroid

View File

@@ -0,0 +1,88 @@
/*
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 { strings } from "@notesnook/intl";
import React from "react";
import { Linking } from "react-native";
import { getVersion } from "react-native-device-info";
import { Update } from "../../../components/sheets/update";
import { presentSheet } from "../../../services/event-manager";
import { SettingSection } from "../types";
export const aboutGroup: SettingSection = {
id: "about",
name: strings.about(),
sections: [
{
id: "download",
name: strings.downloadOnDesktop(),
icon: "download-simple",
iconFamily: "notesnook",
modifer: async () => {
try {
await Linking.openURL("https://notesnook.com/downloads");
} catch (e) {
console.error(e);
}
},
description: strings.downloadOnDesktopDesc()
},
{
id: "roadmap",
name: strings.roadmap(),
icon: "chart-line-up",
iconFamily: "notesnook",
modifer: async () => {
try {
await Linking.openURL("https://notesnook.com/roadmap/");
} catch (e) {
console.error(e);
}
},
description: strings.roadmapDesc()
},
{
id: "check-for-updates",
name: strings.checkForUpdates(),
icon: "device-mobile-camera",
iconFamily: "notesnook",
description: strings.checkForUpdatesDesc(),
modifer: async () => {
presentSheet({
//@ts-ignore // Migrate to ts
component: (ref) => <Update fwdRef={ref} />
});
}
},
{
id: "app-version",
name: strings.appVersion(),
icon: "github-logo",
iconFamily: "notesnook",
modifer: async () => {
try {
await Linking.openURL("https://notesnook.com");
} catch (e) {
console.error(e);
}
},
description: getVersion()
}
]
};

View File

@@ -0,0 +1,74 @@
/*
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 { strings } from "@notesnook/intl";
import { db } from "../../../common/database";
import { MMKV } from "../../../common/database/mmkv";
import { presentDialog } from "../../../components/dialog/functions";
import BiometricService from "../../../services/biometrics";
import Navigation from "../../../services/navigation";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { clearAllStores } from "../../../stores";
import { refreshAllStores } from "../../../stores/create-db-collection-store";
import { useUserStore } from "../../../stores/use-user-store";
import { resetTabStore } from "../../editor/tiptap/use-tab-store";
import { eSendEvent } from "../../../services/event-manager";
import { eAfterSync } from "../../../utils/events";
import { SettingSection } from "../types";
export const accountLocalGroup: SettingSection = {
id: "account-local",
name: strings.account(),
useHook: () => useUserStore((state) => state.user),
hidden: (current) => !!current,
sections: [
{
id: "delete-data",
name: strings.deleteData(),
icon: "trash",
iconFamily: "notesnook",
description: strings.deleteAccountDesc(),
modifer: () => {
presentDialog({
title: strings.deleteData(),
paragraph: strings.irreverisibleAction(),
positiveType: "errorShade",
positiveText: "Delete data",
positivePress: async () => {
await PremiumService.setPremiumStatus();
await BiometricService.resetCredentials();
MMKV.clearStore();
resetTabStore();
clearAllStores();
Navigation.queueRoutesForUpdate();
SettingsService.resetSettings();
db.reset();
setImmediate(() => {
refreshAllStores();
eSendEvent(eAfterSync);
});
return true;
}
});
}
}
]
};

View File

@@ -0,0 +1,842 @@
/*
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 { formatBytes } from "@notesnook/common";
import { SubscriptionPlan, User } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import React from "react";
import { Platform } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import * as RNIap from "react-native-iap";
import { DatabaseLogger, db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import { presentDialog } from "../../../components/dialog/functions";
import {
endProgress,
startProgress
} from "../../../components/dialogs/progress";
import ForceSyncSheet from "../../../components/sheets/force-sync";
import { BackgroundSync } from "../../../services/background-sync";
import BiometricService from "../../../services/biometrics";
import Navigation from "../../../services/navigation";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import {
createFormRef,
validators
} from "../../../components/ui/input/form-input";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
presentSheet
} from "../../../services/event-manager";
import { eCloseSheet } from "../../../utils/events";
import { MFARecoveryCodes, MFASheet } from "../components/2fa";
import { verifyUser } from "../verify-user";
import { logoutUser } from "../logout";
import { SettingSection } from "../types";
import RecoveryKeySheet from "../../../components/sheets/recovery-key";
export const accountGroup: SettingSection = {
id: "account",
name: strings.account(),
useHook: () => useUserStore((state) => state.user),
hidden: (current) => !current,
sections: [
// {
// id: "subscription-status",
// useHook: () => useUserStore((state) => state.user),
// hidden: (current) => {
// const user = current as User;
// return (
// !user ||
// !user.subscription ||
// user.subscription.provider === undefined ||
// !strings.subscriptionProviderInfo[user?.subscription?.provider] ||
// user.subscription?.plan === SubscriptionPlan.FREE
// );
// },
// name: (current) => {
// const user = (current as User) || useUserStore.getState().user;
// return (
// strings.subscriptionProviderInfo[
// user?.subscription?.provider
// ]?.title() || `Unknown provider id: ${user?.subscription?.provider}`
// );
// },
// icon: "credit-card",
// modifer: () => {
// const user = useUserStore.getState().user;
// if (!user) return;
// const subscriptionProviderInfo =
// strings.subscriptionProviderInfo[user?.subscription?.provider];
// if (!subscriptionProviderInfo) return;
// const isCurrentPlatform =
// (user.subscription?.provider === SubscriptionProvider.APPLE &&
// Platform.OS === "ios") ||
// (user.subscription?.provider === SubscriptionProvider.GOOGLE &&
// Platform.OS === "android");
// if (
// (user.subscription?.provider === SubscriptionProvider.GOOGLE ||
// user.subscription?.provider === SubscriptionProvider.APPLE) &&
// isCurrentPlatform &&
// user?.subscription?.productId
// ) {
// RNIap.deepLinkToSubscriptions({
// sku: user?.subscription.productId
// });
// } else {
// presentSheet({
// title: subscriptionProviderInfo.title(),
// paragraph: subscriptionProviderInfo.desc()
// });
// }
// },
// description: (current) => {
// const user = current as User;
// if (!user) return strings.neverHesitate();
// const subscriptionDaysLeft =
// user && getTimeLeft(user.subscription?.expiry);
// const expiryDate = dayjs(user?.subscription?.expiry).format(
// "dddd, MMMM D, YYYY h:mm A"
// );
// const startDate = dayjs(user?.subscription?.start).format(
// "dddd, MMMM D, YYYY h:mm A"
// );
// const trialEndDate = dayjs(user?.subscription?.start)
// .add(
// user?.subscription?.productId?.includes("monthly") ? 7 : 14,
// "day"
// )
// .format("dddd, MMMM D, YYYY h:mm A");
// if (
// user.subscription?.plan !== SubscriptionPlan.FREE &&
// user.subscription?.productId
// ) {
// const status = user.subscription?.status;
// 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();
// }
// return strings.neverHesitate();
// }
// },
{
id: "redeem-gift-code",
name: strings.redeemGiftCode(),
description: strings.redeemGiftCodeDesc(),
hidden: (current) => {
return !current as boolean;
},
useHook: () =>
useUserStore(
(state) => state.user?.subscription?.plan === SubscriptionPlan.FREE
),
icon: "gift",
iconFamily: "notesnook",
modifer: () => {
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()
});
}
},
{
id: "account-settings",
type: "screen",
name: strings.manageAccount(),
component: "account-card",
icon: "user",
iconFamily: "notesnook",
description: strings.manageAccountDesc(),
sections: [
{
id: "account-sub-section",
type: "group",
name: strings.account(),
icon: "user",
iconFamily: "notesnook",
sections: [
{
id: "edit-profile",
type: "screen",
name: strings.editProfile(),
description: strings.editProfileDesc(),
icon: "user",
iconFamily: "notesnook",
sections: [
{
id: "remove-profile-picture",
name: strings.removeProfilePicture(),
description: strings.removeProfilePictureDesc(),
useHook: () =>
useUserStore((state) => state.profile?.profilePicture),
hidden: () =>
!useUserStore.getState().profile?.profilePicture,
modifer: () => {
presentDialog({
title: strings.removeProfilePicture(),
paragraph: strings.removeProfilePictureConfirmation(),
positiveText: strings.remove(),
positivePress: async () => {
db.settings
.setProfile({
profilePicture: undefined
})
.then(async () => {
useUserStore.setState({
profile: db.settings.getProfile()
});
});
}
});
}
},
{
id: "remove-name",
name: strings.removeFullName(),
description: strings.removeFullNameDesc(),
useHook: () =>
useUserStore((state) => state.profile?.fullName),
hidden: () => !useUserStore.getState().profile?.fullName,
modifer: () => {
presentDialog({
title: strings.removeFullName(),
paragraph: strings.removeFullNameConfirmation(),
positiveText: strings.remove(),
positivePress: async () => {
db.settings
.setProfile({
fullName: undefined
})
.then(async () => {
useUserStore.setState({
profile: db.settings.getProfile()
});
});
}
});
}
},
{
id: "change-email",
name: strings.changeEmail(),
type: "screen",
component: "change-email",
description: strings.changeEmailDesc(),
icon: "at"
}
]
},
{
id: "subscription-not-active",
name: strings.subscriptionNotActivated(),
icon: "warning-circle",
iconFamily: "notesnook",
hidden: () => Platform.OS !== "ios",
modifer: async () => {
if (Platform.OS === "android") return;
presentSheet({
title: strings.loadingSubscription(),
paragraph: strings.loadingSubscriptionDesc()
});
const subscriptions = await RNIap.getPurchaseHistory();
subscriptions.sort(
(a, b) => b.transactionDate - a.transactionDate
);
const currentSubscription = subscriptions[0];
presentSheet({
title: strings.notesnookPro(),
paragraph: strings.subscribedOnVerify(
new Date(
currentSubscription.transactionDate
).toLocaleString()
),
action: async () => {
presentSheet({
title: strings.verifySubscription(),
paragraph: strings.subscriptionVerifyWait()
});
await PremiumService.subscriptions.verify(
currentSubscription
);
eSendEvent(eCloseSheet);
},
icon: "information-outline",
actionText: strings.verify()
});
},
description: strings.verifySubDesc()
}
]
},
{
id: "security-sub-section",
type: "group",
name: strings.privacyAndSecurity(),
icon: "two-factor-authentication",
sections: [
{
id: "change-password",
name: strings.changePassword(),
type: "screen",
description: strings.changePasswordDesc(),
component: "change-password",
icon: "pencil-simple-line",
iconFamily: "notesnook"
},
{
id: "2fa-settings",
type: "screen",
name: strings.twoFactorAuth(),
description: strings.twoFactorAuthDesc(),
icon: "shield-check",
iconFamily: "notesnook",
sections: [
{
id: "2fa-settings-group",
name: strings.twoFactorAuth(),
type: "group",
sections: [
{
id: "enable-2fa",
name: strings.change2faMethod(),
icon: "shield-check",
iconFamily: "notesnook",
modifer: () => {
verifyUser("global", async () => {
MFASheet.present();
});
},
useHook: () => useUserStore((state) => state.user),
description: strings.change2faMethodDesc()
},
{
id: "2fa-fallback",
name: strings.addFallback2faMethod(),
icon: "shield-plus",
iconFamily: "notesnook",
useHook: () => useUserStore((state) => state.user),
hidden: (user) => {
return (
!!(user as User)?.mfa?.secondaryMethod ||
!(user as User)?.mfa?.isEnabled
);
},
modifer: () => {
verifyUser("global", async () => {
MFASheet.present(true);
});
},
description: strings.addFallback2faMethodDesc()
},
{
id: "change-2fa-method",
name: strings.change2faFallbackMethod(),
icon: "shield-plus",
useHook: () => useUserStore((state) => state.user),
hidden: (user) => {
return (
!(user as User)?.mfa?.secondaryMethod ||
!(user as User)?.mfa?.isEnabled
);
},
modifer: () => {
verifyUser("global", async () => {
MFASheet.present(true);
});
},
description: strings.change2faFallbackMethod()
},
{
id: "view-2fa-codes",
name: strings.viewRecoveryCodes(),
modifer: () => {
verifyUser("global", async () => {
MFARecoveryCodes.present("sms");
});
},
icon: "numpad",
iconFamily: "notesnook",
useHook: () => useUserStore((state) => state.user),
hidden: (user) => {
return !(user as User)?.mfa?.isEnabled;
},
description: strings.viewRecoveryCodesDesc()
}
]
}
]
},
{
id: "recovery-key",
name: strings.saveDataRecoveryKey(),
iconFamily: "notesnook",
modifer: async () => {
// if (await verifyUser()) {
// }
RecoveryKeySheet.present();
},
description: strings.saveDataRecoveryKeyDesc(),
icon: "key"
}
]
},
{
id: "data-storage-sub-section",
type: "group",
name: strings.dataAndStorage(),
icon: "paperclip",
iconFamily: "notesnook",
sections: [
{
id: "manage-attachments",
name: strings.manageAttachments(),
icon: "paperclip",
iconFamily: "notesnook",
type: "screen",
component: "attachments-manager",
description: strings.manageAttachmentsDesc(),
hideHeader: true
},
{
id: "clear-cache",
name: strings.clearCache(),
icon: "trash",
iconFamily: "notesnook",
modifer: async () => {
presentDialog({
title: strings.clearCacheConfirm(),
paragraph: strings.clearCacheConfirmDesc(),
positiveText: strings.clear(),
positivePress: async () => {
filesystem.clearCache();
ToastManager.show({
heading: strings.cacheCleared(),
message: strings.cacheClearedDesc(),
type: "success"
});
}
});
},
description(current) {
return strings.clearCacheDesc(current as number);
},
useHook: () => {
const [cacheSize, setCacheSize] = React.useState(0);
React.useEffect(() => {
filesystem
.getCacheSize()
.then(setCacheSize)
.catch(() => {
/* empty */
});
const sub = eSubscribeEvent("cache-cleared", () => {
setCacheSize(0);
});
return () => {
sub?.unsubscribe();
};
}, []);
return formatBytes(cacheSize);
}
}
]
},
{
id: "account-actions-sub-section",
type: "group",
name: strings.accountActions(),
icon: "user-sheet-logout",
iconFamily: "notesnook",
sections: [
{
id: "logout",
type: "danger",
name: strings.logout(),
description: strings.logoutWarnin(),
icon: "user-sheet-logout",
iconFamily: "notesnook",
modifer: logoutUser
},
{
id: "delete-account",
type: "danger",
name: strings.deleteAccount(),
icon: "user-circle-minus",
iconFamily: "notesnook",
description: strings.deleteAccountDesc(),
modifer: () => {
presentDialog({
title: strings.deleteAccount(),
paragraphColor: "red",
paragraph: strings.deleteAccountDesc(),
positiveType: "errorShade",
input: true,
secureTextEntry: true,
inputPlaceholder: strings.enterAccountPassword(),
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 () => {
try {
startProgress({
title: "Deleting account",
paragraph:
"Please wait while we delete your account"
});
await db.user?.deleteUser(value);
DatabaseLogger.info("User account deleted");
Navigation.navigate("Notes");
await BiometricService.resetCredentials();
SettingsService.set({
introCompleted: true
});
} catch (e) {
endProgress();
DatabaseLogger.error(e);
ToastManager.error(
e as Error,
strings.failedToDeleteAccount(),
"global"
);
}
}, 300);
} else {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "global"
});
}
} catch (e) {
ToastManager.error(
e as Error,
strings.failedToDeleteAccount(),
"global"
);
}
}
});
}
}
]
}
]
},
{
id: "sync-settings",
name: strings.syncSettings(),
description: strings.syncSettingsDesc(),
type: "screen",
icon: "arrows-clockwise",
iconFamily: "notesnook",
component: "offline-mode-progress",
sections: [
{
id: "sync-behavior-sub-section",
type: "group",
name: strings.syncBehavior(),
sections: [
{
id: "auto-sync",
name: strings.autoSync(),
description: strings.autoSyncDesc(),
type: "switch",
property: "disableAutoSync",
featureId: "syncControls",
icon: "arrows-clockwise",
iconFamily: "notesnook"
},
{
id: "disable-realtime-sync",
name: strings.realtimeSync(),
description: strings.realtimeSyncDesc(),
type: "switch",
property: "disableRealtimeSync",
featureId: "syncControls",
icon: "cloud-check",
iconFamily: "notesnook"
},
{
id: "offline-mode",
icon: "wifi-slash",
iconFamily: "notesnook",
name: strings.fullOfflineMode(),
description: strings.fullOfflineModeDesc(),
type: "switch",
property: "offlineMode",
featureId: "fullOfflineMode",
modifer: () => {
const current = SettingsService.get().offlineMode;
if (current) {
SettingsService.setProperty("offlineMode", false);
db.fs().cancel("offline-mode");
return;
}
SettingsService.setProperty("offlineMode", true);
db.attachments.cacheAttachments().catch(() => {
/* empty */
});
}
}
]
},
{
id: "advanced-sync-sub-section",
type: "group",
name: strings.advancedSettings(),
sections: [
{
id: "background-sync",
name: strings.backgroundSync(),
description: strings.backgroundSyncDesc(),
type: "switch",
property: "backgroundSync",
icon: "arrow-u-up-left",
iconFamily: "notesnook",
onChange: (value) => {
if (value) {
BackgroundSync.start();
} else {
BackgroundSync.stop();
}
}
}
]
},
{
id: "sync-troubleshooting-sub-section",
type: "group",
name: strings.troubleshooting(),
sections: [
{
id: "disable-sync",
name: strings.pauseSync(),
description: strings.pauseSyncDesc(),
type: "switch",
property: "disableSync",
featureId: "syncControls",
icon: "pause",
iconFamily: "notesnook"
},
{
id: "pull-sync",
name: strings.forcePullChanges(),
description: strings.forcePullChangesDesc(),
icon: "git-pull-request",
iconFamily: "notesnook",
modifer: () => {
ForceSyncSheet.present("fetch");
}
},
{
id: "push-sync",
name: strings.forcePushChanges(),
description: strings.forcePushChangesDesc(),
icon: "arrow-fat-up",
iconFamily: "notesnook",
modifer: () => {
ForceSyncSheet.present("send");
}
}
]
}
]
},
{
id: "notesnook-circle",
name: strings.notesnookCircle(),
icon: "users-three",
iconFamily: "notesnook",
type: "screen",
description: strings.notesnookCircleDesc(),
component: "notesnook-circle"
},
{
id: "inbox-api",
name: strings.inboxAPI(),
icon: "inbox",
type: "screen",
description: strings.inboxAPIDesc(),
sections: [
{
id: "inbox-api-group",
name: strings.inboxAPI(),
type: "group",
sections: [
{
id: "toggle-inbox-api",
name: strings.enableInboxAPI(),
description: strings.enableInboxAPIDesc(),
type: "switch",
icon: "file-cloud",
iconFamily: "notesnook",
useHook: () => {
return useSettingStore((state) => state.inboxEnabled);
},
getter: (current) => current,
modifer: async (current) => {
if (current) {
return new Promise((resolve) => {
presentDialog({
title: strings.disableInboxAPI(),
paragraph: strings.disableInboxAPIDesc(),
positiveText: strings.disable(),
onClose: () => {
resolve();
},
positivePress: async () => {
try {
await db.inboxItemsHistory.deleteFailed();
await db.user.discardInboxKeys();
useSettingStore.setState({
inboxEnabled: false
});
resolve();
return true;
} catch (e) {
ToastManager.show({
message: (e as Error).message,
context: "local"
});
DatabaseLogger.error(e);
return false;
}
}
});
});
}
try {
Navigation.push("SettingsGroup", {
id: "setup-inbox-keys",
name: strings.setupInboxKeys(),
type: "screen",
component: "setup-inbox-keys"
} as any);
} catch (e) {
console.log(e);
}
}
},
{
id: "manage-inbox-keys",
name: strings.manageInboxKeys(),
icon: "tray-arrow-down",
iconFamily: "notesnook",
useHook: () => useSettingStore((state) => state.inboxEnabled),
hidden: (current) => !current,
description: strings.manageInboxKeysDesc(),
onVerify: async () => {
return new Promise((resolve) => {
verifyUser(
"global",
() => {
resolve(true);
},
false,
() => resolve(false)
);
});
},
type: "screen",
component: "manage-inbox-keys"
},
{
id: "inbox-keys",
name: strings.viewAPIKeys(),
description: strings.viewAPIKeysDesc(),
useHook: () => useSettingStore((state) => state.inboxEnabled),
hidden: (current) => !current,
type: "screen",
component: "inbox-keys",
icon: "key",
iconFamily: "notesnook"
},
{
id: "failed-inbox-items",
name: strings.failedInboxItems(),
description: strings.failedInboxItemsDesc(),
useHook: () => useSettingStore((state) => state.inboxEnabled),
hidden: (current) => !current,
type: "screen",
component: "failed-inbox-items",
icon: "warning-circle",
iconFamily: "notesnook",
hideHeader: true
}
]
}
]
}
]
};

View File

@@ -0,0 +1,313 @@
/*
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 { strings } from "@notesnook/intl";
import { Platform } from "react-native";
import ExportNotesSheet from "../../../components/sheets/export-notes";
import BackupService from "../../../services/backup";
import SettingsService from "../../../services/settings";
import { useUserStore } from "../../../stores/use-user-store";
import { ToastManager } from "../../../services/event-manager";
import { verifyUser } from "../verify-user";
import { SettingSection } from "../types";
import { restoreBackup } from "../restore-backup";
import { keepLocalCopy, pick } from "@react-native-documents/picker";
import * as ScopedStorage from "react-native-scoped-storage";
export const backRestoreGroup: SettingSection = {
id: "back-restore",
name: strings.backupRestore(),
sections: [
{
id: "backups",
type: "screen",
name: strings.backups(),
icon: "clock-counter-clockwise",
iconFamily: "notesnook",
description: strings.backupsDesc(),
sections: [
{
id: "backup-actions-group",
type: "group",
name: strings.backupActions(),
sections: [
{
id: "backup-now",
name: strings.backupNow(),
description: strings.backupNowDesc(),
icon: "clock-counter-clockwise",
iconFamily: "notesnook",
modifer: async () => {
const user = useUserStore.getState().user;
if (!user || SettingsService.getProperty("encryptedBackup")) {
await BackupService.run(true);
return;
}
verifyUser(undefined, () => BackupService.run(true));
}
},
{
id: "backup-now-with-attachments",
name: strings.fullBackup(),
description: strings.backupNowWithAttachmentsDesc(),
icon: "cloud-check",
iconFamily: "notesnook",
hidden: () => !useUserStore.getState().user,
modifer: async () => {
const user = useUserStore.getState().user;
if (!user || SettingsService.getProperty("encryptedBackup")) {
await BackupService.run(true, undefined, "full");
return;
}
verifyUser(undefined, () =>
BackupService.run(true, undefined, "full")
);
}
}
]
},
{
id: "auto-backups-group",
name: strings.automaticBackups(),
type: "group",
sections: [
{
id: "auto-backups",
type: "component",
icon: "clock",
iconFamily: "notesnook",
name: strings.automaticBackups(),
description: strings.automaticBackupsDesc(),
component: "autobackups"
},
{
id: "auto-backups-with-attachments",
type: "component",
icon: "arrows-clockwise",
iconFamily: "notesnook",
hidden: () => !useUserStore.getState().user,
name: strings.automaticFullBackup(),
description: [
...strings.automaticBackupsWithAttachmentsDesc()
].join("\n"),
component: "autobackupsattachments"
}
]
},
{
id: "storage-location-group",
name: strings.storageAndLocation(),
type: "group",
hidden: () => Platform.OS !== "android",
sections: [
{
id: "select-backup-dir",
name: strings.selectBackupDir(),
description: () => {
const desc = strings.selectBackupDirDesc(
SettingsService.get().backupDirectoryAndroid?.path || ""
);
return desc[0] + " " + desc[1];
},
icon: "folder",
iconFamily: "notesnook",
hidden: () =>
!!SettingsService.get().backupDirectoryAndroid ||
Platform.OS !== "android",
property: "backupDirectoryAndroid",
modifer: async () => {
let dir;
try {
dir = await BackupService.checkBackupDirExists(true);
} catch (e) {
console.error(e);
} finally {
if (!dir) {
ToastManager.show({
heading: strings.noDirectorySelected(),
type: "error"
});
}
}
}
},
{
id: "change-backup-dir",
name: strings.changeBackupDir(),
description: () =>
SettingsService.get().backupDirectoryAndroid?.name || "",
icon: "folder",
hidden: () =>
!SettingsService.get().backupDirectoryAndroid ||
Platform.OS !== "android",
property: "backupDirectoryAndroid",
modifer: async () => {
let dir;
try {
dir = await BackupService.checkBackupDirExists(true);
} catch (e) {
console.error(e);
} finally {
if (!dir) {
ToastManager.show({
heading: strings.noDirectorySelected(),
type: "error"
});
}
}
}
}
]
},
{
id: "backup-encryption-group",
type: "group",
name: strings.security(),
sections: [
{
id: "enable-backup-encryption",
type: "switch",
name: strings.backupEncryption(),
description: strings.backupEncryptionDesc(),
icon: "lock",
property: "encryptedBackup",
modifer: async () => {
const user = useUserStore.getState().user;
const settings = SettingsService.get();
if (!user) {
ToastManager.show({
heading: strings.loginRequired(),
type: "error"
});
return;
}
if (settings.encryptedBackup) {
await verifyUser(undefined, () => {
SettingsService.set({
encryptedBackup: false
});
});
} else {
SettingsService.set({
encryptedBackup: true
});
}
}
}
]
}
]
},
{
id: "restore-backup",
name: strings.restoreBackup(),
description: strings.restoreBackupDesc(),
icon: "arrow-counter-clockwise",
iconFamily: "notesnook",
type: "screen",
sections: [
{
id: "restore-options",
type: "group",
name: strings.restoreActions(),
sections: [
{
id: "restore-from-files",
name: strings.restoreFromFiles(),
icon: "folder",
modifer: async () => {
useUserStore.setState({
disableAppLockRequests: true
});
const file = await pick();
const fileCopy = await keepLocalCopy({
destination: "cachesDirectory",
files: [
{
uri: file[0].uri,
fileName: file[0].name ?? `backup_restore_${Date.now()}`
}
]
});
if (fileCopy[0].status === "error") {
ToastManager.error(new Error("File copy error"));
return;
}
setTimeout(() => {
useUserStore.setState({
disableAppLockRequests: false
});
}, 1000);
restoreBackup({
uri: fileCopy[0].localUri,
deleteFile: true
});
},
description: strings.selectBackupFileDesc()
},
{
id: "select-backup-folder",
name: strings.selectBackupFolder(),
icon: "folder",
hidden: () => Platform.OS !== "android",
modifer: async () => {
const folder = await ScopedStorage.openDocumentTree(true);
let subfolder;
if (folder.name !== "Notesnook backups") {
subfolder = await ScopedStorage.createDirectory(
folder.uri,
"Notesnook backups"
);
} else {
subfolder = folder;
}
SettingsService.set({
backupDirectoryAndroid: subfolder
});
},
description: strings.selectFolderForBackupFilesDesc()
}
]
},
{
id: "recent-backups-group",
type: "component",
component: "backuprestore"
}
]
},
{
id: "export-notes",
name: strings.exportAllNotes(),
icon: "export",
iconFamily: "notesnook",
description: strings.exportAllNotesDesc(),
modifer: () => {
verifyUser(undefined, () => {
ExportNotesSheet.present(undefined, true);
});
}
}
]
};

View File

@@ -0,0 +1,76 @@
/*
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 { strings } from "@notesnook/intl";
import { Linking } from "react-native";
import { SettingSection } from "../types";
export const communityGroup: SettingSection = {
id: "community",
name: strings.community(),
sections: [
{
id: "join-telegram",
name: strings.joinTelegram(),
description: strings.joinTelegramDesc(),
icon: "telegram-logo",
iconFamily: "notesnook",
iconSize: 16,
modifer: () => {
Linking.openURL("https://t.me/notesnook").catch(() => {
/* empty */
});
}
},
{
id: "join-mastodon",
name: strings.joinMastodon(),
description: strings.joinMastodonDesc(),
icon: "mastodon-logo",
iconFamily: "notesnook",
modifer: () => {
Linking.openURL("https://fosstodon.org/@notesnook").catch(console.log);
}
},
{
id: "join-twitter",
name: strings.followOnX(),
description: strings.followOnXDesc(),
icon: "x-logo",
iconFamily: "notesnook",
modifer: () => {
Linking.openURL("https://twitter.com/notesnook").catch(() => {
/* empty */
});
}
},
{
id: "join-discord",
name: strings.joinDiscord(),
icon: "discord-logo",
iconFamily: "notesnook",
modifer: async () => {
Linking.openURL("https://discord.gg/zQBK97EE22").catch(() => {
/* empty */
});
},
description: strings.joinDiscordDesc()
}
]
};

View File

@@ -0,0 +1,345 @@
/*
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 { strings } from "@notesnook/intl";
import { Appearance } from "react-native";
import { db } from "../../../common/database";
import DateFormat from "../../../components/sheets/date-format";
import TrashInterval from "../../../components/sheets/trash-interval";
import SettingsService from "../../../services/settings";
import { useThemeStore } from "../../../stores/use-theme-store";
import { EDITOR_LINE_HEIGHT } from "../../../utils/constants";
import { ToastManager } from "../../../services/event-manager";
import { useDragState } from "../components/editor/state";
import { SettingSection } from "../types";
export const customizeGroup: SettingSection = {
id: "customize",
name: strings.customization(),
sections: [
{
id: "personalization",
type: "screen",
name: strings.appearance(),
description: strings.appearanceDesc(),
icon: "squares-four",
iconFamily: "notesnook",
sections: [
{
id: "appearance-group",
type: "group",
name: strings.appearance(),
sections: [
{
id: "theme-picker",
type: "screen",
name: strings.themes(),
description: strings.themesDesc(),
component: "theme-selector",
icon: "paint-roller",
iconFamily: "notesnook"
},
{
id: "use-system-theme",
type: "switch",
name: strings.useSystemTheme(),
description: strings.useSystemThemeDesc(),
property: "useSystemTheme",
icon: "swatches",
iconFamily: "notesnook",
modifer: () => {
const current = SettingsService.get().useSystemTheme;
SettingsService.set({
useSystemTheme: !current
});
if (!current) {
const systemColorScheme = Appearance.getColorScheme();
useThemeStore
.getState()
.setColorScheme(
systemColorScheme === "dark" ? "dark" : "light"
);
}
}
},
{
id: "enable-dark-mode",
type: "switch",
name: strings.darkMode(),
description: strings.darkModeDesc(),
property: "colorScheme",
icon: "moon",
iconFamily: "notesnook",
modifer: () => {
useThemeStore.getState().setColorScheme();
},
getter: () => useThemeStore.getState().colorScheme === "dark"
}
]
}
]
},
{
id: "behaviour",
type: "screen",
icon: "sliders-horizontal",
iconFamily: "notesnook",
name: strings.behavior(),
description: strings.behaviorDesc(),
sections: [
{
id: "navigation-group",
type: "group",
name: strings.navigation(),
sections: [
{
id: "default-sidebar-view",
type: "component",
name: strings.defaultSidebarTab(),
description: strings.defaultSidebarTabDesc(),
component: "sidebar-tab-selector",
icon: "table",
iconFamily: "notesnook"
}
]
},
{
id: "date-time-group",
name: strings.dateAndTime(),
type: "group",
sections: [
{
id: "date-format",
name: strings.dateFormat(),
description: strings.dateFormatDesc(),
icon: "calendar",
iconFamily: "notesnook",
modifer: () => {
DateFormat.present();
}
},
{
id: "day-format",
name: strings.dayFormat(),
description: strings.dayFormatDesc(),
type: "component",
component: "day-format-selector",
icon: "calendar-check",
iconFamily: "notesnook"
},
{
id: "week-format",
name: strings.weekFormat(),
description: strings.weekFormatDesc(),
type: "component",
component: "week-format-selector",
icon: "calendar-dots",
iconFamily: "notesnook"
},
{
id: "time-format",
name: strings.timeFormat(),
description: strings.timeFormatDesc(),
type: "component",
component: "time-format-selector",
icon: "clock",
iconFamily: "notesnook"
}
]
},
{
id: "miscellaneous-group",
type: "group",
name: strings.miscellaneous(),
sections: [
{
id: "clear-trash-interval",
name: strings.clearTrashInterval(),
description: strings.clearTrashIntervalDesc(),
icon: "trash",
iconFamily: "notesnook",
modifer: () => {
TrashInterval.present();
}
},
{
id: "image-compression",
type: "component",
name: strings.imageCompression(),
description: strings.imageCompressionDesc(),
component: "image-compression-picker",
icon: "arrows-clockwise",
iconFamily: "notesnook"
},
{
id: "default-notebook",
name: strings.clearDefaultNotebook(),
description: strings.clearDefaultNotebookDesc(),
modifer: () => {
db.settings.setDefaultNotebook(undefined);
ToastManager.show({
heading: strings.defaultNotebookCleared(),
type: "success"
});
},
hidden: () => !db.settings.getDefaultNotebook(),
icon: "bookmark",
iconFamily: "notesnook"
},
{
id: "disable-update-check",
type: "switch",
name: strings.autoUpdateCheck(),
description: strings.autoUpdateCheckDesc(),
property: "checkForUpdates",
icon: "arrow-clockwise",
iconFamily: "notesnook"
}
]
}
]
},
{
id: "editor",
name: strings.editor(),
type: "screen",
icon: "pencil-simple-line",
iconFamily: "notesnook",
description: strings.editorDesc(),
sections: [
{
id: "toolbar-group",
name: strings.toolbar(),
type: "group",
sections: [
{
id: "configure-toolbar",
type: "screen",
name: strings.customizeToolbar(),
description: strings.customizeToolbarDesc(),
component: "configuretoolbar"
},
{
id: "reset-toolbar",
name: strings.resetToolbar(),
description: strings.resetToolbarDesc(),
modifer: () => {
useDragState.getState().setPreset("default");
ToastManager.show({
heading: strings.toolbarReset(),
type: "success"
});
}
}
]
},
{
id: "typography-group",
name: strings.typography(),
type: "group",
sections: [
{
id: "default-font-family",
name: strings.defaultFontFamily(),
description: strings.defaultFontFamilyDesc(),
type: "component",
icon: "format-font",
property: "defaultFontFamily",
component: "font-selector"
},
{
id: "default-font-size",
name: strings.defaultFontSize(),
description: strings.defaultFontSizeDesc(),
type: "input-selector",
minInputValue: 8,
maxInputValue: 120,
inputBadgeValue: "px",
icon: "format-size",
property: "defaultFontSize"
},
{
id: "default-line-height",
name: strings.lineHeight(),
description: strings.lineHeightDesc(),
type: "input-selector",
property: "defaultLineHeight",
icon: "format-line-spacing",
minInputValue: EDITOR_LINE_HEIGHT.MIN,
maxInputValue: EDITOR_LINE_HEIGHT.MAX
}
]
},
{
id: "formatting-group",
name: strings.formatting(),
type: "group",
sections: [
{
id: "double-spaced-lines",
name: strings.doubleSpacedLines(),
description: strings.doubleSpacedLinesDesc(),
type: "switch",
property: "doubleSpacedLines",
icon: "format-line-spacing",
onChange: () => {
ToastManager.show({
heading: strings.lineSpacingChanged(),
type: "success"
});
}
},
{
id: "toggle-markdown",
name: strings.mardownShortcuts(),
property: "markdownShortcuts",
description: strings.mardownShortcutsDesc(),
type: "switch",
featureId: "markdownShortcuts"
}
]
},
{
id: "note-title-group",
type: "group",
name: strings.newNoteTitle(),
sections: [
{
id: "title-format",
name: strings.titleFormat(),
component: "title-format",
description: strings.titleFormatDesc(),
type: "component"
}
]
}
]
},
{
id: "servers",
type: "screen",
name: strings.servers(),
description: strings.serversConfigurationDesc(),
icon: "hard-drives",
iconFamily: "notesnook",
component: "server-config"
}
]
};

View File

@@ -0,0 +1,84 @@
/*
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 { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React from "react";
import { Linking } from "react-native";
import DownloadLogs from "../../../components/sheets/download-logs";
import { Issue } from "../../../components/sheets/github/issue";
import { ToastManager, presentSheet } from "../../../services/event-manager";
import { SettingSection } from "../types";
export const helpSupportGroup: SettingSection = {
id: "help-support",
name: strings.helpAndSupport(),
sections: [
{
id: "report-issue",
name: strings.reportAnIssue(),
icon: "warning-circle",
iconFamily: "notesnook",
modifer: () => {
presentSheet({
//@ts-ignore Migrate to TS
component: <Issue />
});
},
description: strings.reportAnIssueDesc()
},
{
id: "email-support",
name: strings.emailSupport(),
icon: "envelope-simple",
iconFamily: "notesnook",
modifer: () => {
Clipboard.setString("support@streetwriters.co");
ToastManager.show({
heading: strings.emailCopied(),
type: "success",
icon: "content-copy"
});
setTimeout(() => {
Linking.openURL("mailto:support@streetwriters.co");
}, 1000);
},
description: strings.emailSupportDesc()
},
{
id: "docs-link",
name: strings.documentation(),
modifer: async () => {
Linking.openURL("https://help.notesnook.com/");
},
description: strings.documentationDesc(),
icon: "file-text",
iconFamily: "notesnook"
},
{
id: "debugging",
name: strings.downloadDebugLogs(),
description: strings.downloadDebugLogsDesc(),
icon: "bug-droid",
iconFamily: "notesnook",
modifer: () => {
DownloadLogs.present();
}
}
]
};

View File

@@ -0,0 +1,48 @@
/*
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 from "react";
import { SettingSection } from "../types";
import { accountLocalGroup } from "./account-local";
import { accountGroup } from "./account";
import { customizeGroup } from "./customize";
import { privacySecurityGroup } from "./privacy-security";
import { backRestoreGroup } from "./back-restore";
import { productivityGroup } from "./productivity";
import { helpSupportGroup } from "./help-support";
import { communityGroup } from "./community";
import { legalGroup } from "./legal";
import { aboutGroup } from "./about";
export const useSettingsData = () => {
return React.useMemo<SettingSection[]>(
() => [
accountLocalGroup,
accountGroup,
customizeGroup,
privacySecurityGroup,
backRestoreGroup,
productivityGroup,
helpSupportGroup,
communityGroup,
legalGroup,
aboutGroup
],
[]
);
};

View File

@@ -0,0 +1,66 @@
/*
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 { strings } from "@notesnook/intl";
import { Linking } from "react-native";
import { SettingSection } from "../types";
export const legalGroup: SettingSection = {
id: "legal",
name: strings.legal(),
sections: [
{
id: "tos",
name: strings.tos(),
icon: "bag-simple",
iconFamily: "notesnook",
modifer: async () => {
try {
await Linking.openURL("https://notesnook.com/tos");
} catch (e) {
console.error(e);
}
},
description: strings.tosDesc()
},
{
id: "privacy-policy",
name: strings.privacyPolicy(),
icon: "shield",
iconFamily: "notesnook",
modifer: async () => {
try {
await Linking.openURL("https://notesnook.com/privacy");
} catch (e) {
console.error(e);
}
},
description: strings.privacyPolicyDesc()
},
{
id: "licenses",
name: strings.licenses(),
type: "screen",
component: "licenses",
description: strings.ossLibs(),
icon: "file-dashed",
iconFamily: "notesnook"
}
]
};

View File

@@ -0,0 +1,374 @@
/*
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 { strings } from "@notesnook/intl";
import { db } from "../../../common/database";
import { AppLockPassword } from "../../../components/dialogs/applock-password";
import AppLockTimeout from "../../../components/sheets/app-lock-timeout";
import LockVaultTimer from "../../../components/sheets/lock-vault-timer";
import {
VaultStatusType,
useVaultStatus
} from "../../../hooks/use-vault-status";
import BiometricService from "../../../services/biometrics";
import SettingsService from "../../../services/settings";
import { useUserStore } from "../../../stores/use-user-store";
import {
ToastManager,
VaultRequestType,
openVault
} from "../../../services/event-manager";
import { verifyUserWithApplock } from "../verify-user";
import { SettingSection } from "../types";
export const privacySecurityGroup: SettingSection = {
id: "privacy-security",
name: strings.privacyAndSecurity(),
sections: [
{
id: "marketing-emails",
type: "switch",
icon: "envelope-simple",
iconFamily: "notesnook",
name: strings.marketingEmails(),
description: strings.marketingEmailsDesc(),
modifer: async () => {
try {
await db.user?.changeMarketingConsent(
!useUserStore.getState().user?.marketingConsent
);
useUserStore.getState().setUser(await db.user?.fetchUser());
} catch (e) {
ToastManager.error(e as Error);
}
},
getter: (current: any) => current?.marketingConsent,
useHook: () => useUserStore((state) => state.user),
hidden: (current) => !current
},
{
id: "cors-bypass",
type: "input",
name: strings.corsBypass(),
description: strings.corsBypassDesc(),
inputProperties: {
defaultValue: "https://cors.notesnook.com",
autoCorrect: false,
keyboardType: "url"
},
property: "corsProxy",
icon: "network",
iconFamily: "notesnook"
},
{
id: "vault",
type: "screen",
name: strings.vault(),
description: strings.vaultDesc(),
icon: "key",
iconFamily: "notesnook",
sections: [
{
id: "vault-group",
name: strings.vault(),
type: "group",
sections: [
{
id: "create-vault",
name: strings.createVault(),
description: strings.createVaultDesc(),
icon: "key",
iconFamily: "notesnook",
useHook: useVaultStatus,
hidden: (current) => (current as VaultStatusType)?.exists,
modifer: () => {
openVault({
requestType: VaultRequestType.CreateVault,
title: strings.createVault(),
buttonTitle: strings.create(),
positiveButtonType: "accent"
});
}
},
{
id: "biometric-unlock",
type: "switch",
name: strings.biometricUnlock(),
icon: "fingerprint-simple",
iconFamily: "notesnook",
useHook: useVaultStatus,
description: strings.biometricUnlockDesc(),
hidden: (current) => {
const _current = current as VaultStatusType;
return !_current?.exists || !_current?.isBiometryAvailable;
},
getter: (current) =>
(current as VaultStatusType)?.biometryEnrolled,
modifer: (current) => {
const _current = current as VaultStatusType;
const isRevoking = _current.biometryEnrolled;
openVault({
requestType: isRevoking
? VaultRequestType.RevokeFingerprint
: VaultRequestType.EnableFingerprint,
title: isRevoking
? strings.revokeBiometricUnlock()
: strings.enableBiometricUnlock(),
buttonTitle: isRevoking ? strings.revoke() : strings.enable(),
positiveButtonType: "accent"
});
}
},
{
id: "change-vault-password",
useHook: useVaultStatus,
name: strings.changeVaultPassword(),
icon: "pencil-simple",
iconFamily: "notesnook",
description: strings.changeVaultPasswordDesc(),
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () =>
openVault({
requestType: VaultRequestType.ChangePassword,
title: strings.changeVaultPassword(),
buttonTitle: strings.change(),
positiveButtonType: "accent"
})
},
{
id: "lock-vault-after",
useHook: useVaultStatus,
name: strings.lockVaultAfter(),
description: strings.lockVaultAfterDesc(),
hidden: (current) => !(current as VaultStatusType)?.exists,
icon: "clock",
iconFamily: "notesnook",
modifer: () => {
LockVaultTimer.present();
}
},
{
id: "clear-vault",
useHook: useVaultStatus,
description: strings.clearVaultDesc(),
name: strings.clearVault(),
icon: "paint-brush-household",
iconFamily: "notesnook",
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () => {
openVault({
requestType: VaultRequestType.ClearVault,
title: strings.clearVault() + "?",
buttonTitle: strings.clear(),
positiveButtonType: "accent",
icon: "warning-circle"
});
}
},
{
id: "delete-vault",
name: strings.deleteVault(),
description: strings.deleteVaultDesc(),
icon: "trash",
iconFamily: "notesnook",
type: "danger",
useHook: useVaultStatus,
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () => {
openVault({
requestType: VaultRequestType.DeleteVault,
title: strings.deleteVault() + "?",
buttonTitle: strings.delete(),
positiveButtonType: "accent",
icon: "warning-circle"
});
}
}
]
}
]
},
{
id: "privacy-mode",
type: "switch",
icon: "eye-slash",
iconFamily: "notesnook",
name: strings.privacyMode(),
description: strings.privacyModeDesc(),
modifer: () => {
const settings = SettingsService.get();
SettingsService.setPrivacyScreen(!settings.privacyScreen);
SettingsService.set({ privacyScreen: !settings.privacyScreen });
},
property: "privacyScreen"
},
{
id: "app-lock",
name: strings.appLock(),
type: "screen",
description: strings.appLockDesc(),
icon: "lock-simple",
iconFamily: "notesnook",
featureId: "appLock",
sections: [
{
id: "app-lock-group",
type: "group",
name: strings.appLock(),
sections: [
{
id: "app-lock-mode",
name: strings.enableAppLock(),
description: strings.appLockDesc(),
icon: "lock",
iconFamily: "notesnook",
type: "switch",
property: "appLockEnabled",
featureId: "appLock",
onChange: (property) => {
if (property) {
SettingsService.set({
privacyScreen: true
});
SettingsService.setPrivacyScreen(true);
}
},
onVerify: async () => {
const verified = (await verifyUserWithApplock()) as boolean;
if (!verified) return false;
if (!SettingsService.getProperty("appLockEnabled")) {
if (
!SettingsService.getProperty(
"appLockHasPasswordSecurity"
) &&
(await BiometricService.isBiometryAvailable())
) {
SettingsService.setProperty("biometricsAuthEnabled", true);
}
if (
!(await BiometricService.isBiometryAvailable()) &&
!SettingsService.getProperty("appLockHasPasswordSecurity")
) {
AppLockPassword.present("create", true);
return false;
}
}
return verified;
}
},
{
id: "app-lock-timer",
name: strings.appLockTimeout(),
description: strings.appLockTimeoutDesc(),
icon: "clock",
iconFamily: "notesnook",
modifer: async () => {
if (await verifyUserWithApplock()) {
AppLockTimeout.present();
}
}
},
{
id: "app-lock-pass-setup",
icon: "numpad",
iconFamily: "notesnook",
name: strings.setupAppLockPassword(),
description: strings.setupAppLockPasswordDesc(),
hidden: () => {
return !!SettingsService.getProperty(
"appLockHasPasswordSecurity"
);
},
onVerify: () => {
return verifyUserWithApplock();
},
property: "appLockHasPasswordSecurity",
modifer: () => {
AppLockPassword.present("create");
}
},
{
id: "app-lock-pass-change",
icon: "numpad",
iconFamily: "notesnook",
name: strings.changeAppLockPassword(),
description: strings.changeAppLockPasswordDesc(),
hidden: () => {
return !SettingsService.getProperty(
"appLockHasPasswordSecurity"
);
},
property: "appLockHasPasswordSecurity",
modifer: () => {
AppLockPassword.present("change");
}
},
{
id: "app-lock-pass-remove",
name: strings.removeAppLockPassword(),
description: strings.removeAppLockPasswordDesc(),
hidden: () => {
return !SettingsService.getProperty(
"appLockHasPasswordSecurity"
);
},
icon: "backspace",
iconFamily: "notesnook",
property: "appLockHasPasswordSecurity",
modifer: () => {
AppLockPassword.present("remove");
}
},
{
id: "app-lock-fingerprint",
name: strings.unlockWithBiometrics(),
description: strings.unlockWithBiometricsDesc(),
type: "switch",
property: "biometricsAuthEnabled",
onVerify: async () => {
const verified = await verifyUserWithApplock();
if (!verified) return false;
if (SettingsService.getProperty("biometricsAuthEnabled")) {
if (
!SettingsService.getProperty("appLockHasPasswordSecurity")
) {
SettingsService.setProperty("appLockEnabled", false);
ToastManager.show({
heading: strings.appLockDisabled(),
type: "success"
});
}
}
return verified;
},
icon: "fingerprint-simple",
iconFamily: "notesnook"
}
]
}
]
}
]
};

View File

@@ -0,0 +1,131 @@
/*
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 { strings } from "@notesnook/intl";
import notifee from "@notifee/react-native";
import { Platform } from "react-native";
import Notifications from "../../../services/notifications";
import SettingsService from "../../../services/settings";
import { SettingSection } from "../types";
export const productivityGroup: SettingSection = {
id: "productivity",
name: strings.productivity(),
sections: [
{
id: "notification-notes",
type: "switch",
name: strings.quickNoteNotification(),
description: strings.quickNoteNotificationDesc(),
property: "notifNotes",
icon: "notification",
iconFamily: "notesnook",
modifer: async () => {
const settings = SettingsService.get();
if (settings.notifNotes) {
Notifications.unpinQuickNote();
} else {
Notifications.pinQuickNote();
}
SettingsService.set({
notifNotes: !settings.notifNotes
});
},
hidden: () => Platform.OS !== "android",
featureId: "createNoteFromNotificationDrawer"
},
{
id: "reminders",
type: "screen",
name: strings.reminders(),
icon: "bell",
iconFamily: "notesnook",
description: strings.remindersDesc(),
sections: [
{
id: "reminder-notifications",
type: "group",
name: strings.notifications(),
sections: [
{
id: "enable-reminders",
property: "reminderNotifications",
type: "switch",
name: strings.reminderNotification(),
icon: "bell-outline",
onChange: (property) => {
if (property) {
Notifications.setupReminders();
} else {
Notifications.clearAllTriggers();
}
},
description: strings.reminderNotificationDesc()
},
{
id: "snooze-time",
property: "defaultSnoozeTime",
type: "input",
icon: "bell-z",
iconFamily: "notesnook",
name: strings.defaultSnoozeTime(),
description: strings.defaultSnoozeTimeDesc(),
inputProperties: {
keyboardType: "decimal-pad",
defaultValue: 5 + "",
placeholder: strings.setSnoozeTimePlaceholder(),
onSubmitEditing: () => {
Notifications.setupReminders();
}
}
},
{
id: "reminder-sound-ios",
type: "screen",
name: strings.changeNotificationSound(),
description: strings.changeNotificationSoundDesc(),
component: "sound-picker",
icon: "speaker-high",
iconFamily: "notesnook",
hidden: () =>
Platform.OS === "ios" ||
(Platform.OS === "android" && Platform.Version > 25)
},
{
id: "reminder-sound-android",
name: strings.changeNotificationSound(),
description: strings.changeNotificationSoundDesc(),
icon: "speaker-high",
iconFamily: "notesnook",
hidden: () =>
Platform.OS === "ios" ||
(Platform.OS === "android" && Platform.Version < 26),
modifer: async () => {
const id = await Notifications.getChannelId("urgent");
if (id) {
await notifee.openNotificationSettings(id);
}
}
}
]
}
]
}
]
};

View File

@@ -25,16 +25,21 @@ import { Header } from "../../components/header";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import useNavigationStore from "../../stores/use-navigation-store";
import { SectionGroup } from "./section-group";
import { settingsGroups } from "./settings-data";
import { useSettingsData } from "./settings-data";
import { RouteParams, SettingSection } from "./types";
import SettingsUserSection from "./user-section";
import SettingsUserSection from "./components/user-section";
import { LegendList } from "@legendapp/list";
import { useThemeColors } from "@notesnook/theme";
import { Spacing } from "../../common/design/spacing";
import { View } from "react-native";
const keyExtractor = (item: SettingSection) => item.id;
const Home = ({
navigation
}: NativeStackScreenProps<RouteParams, "SettingsHome">) => {
const { colors } = useThemeColors();
const settingsGroups = useSettingsData();
useNavigationFocus(navigation, {
onFocus: () => {
useNavigationStore.getState().setFocusedRouteId("Settings");
@@ -43,20 +48,32 @@ const Home = ({
focusOnInit: true
});
const renderItem = ({ item }: { item: SettingSection; index: number }) =>
const renderItem = ({
item,
index
}: {
item: SettingSection;
index: number;
}) =>
item.id === "account" ? (
<SettingsUserSection item={item} />
) : (
<SectionGroup item={item} />
<SectionGroup item={item} isLast={!settingsGroups[index + 1]} />
);
return (
<>
<View>
<Header
renderedInRoute="Settings"
title={strings.routes.Settings()}
canGoBack={true}
hasSearch={false}
style={{
backgroundColor: "transparent",
borderRadius: 0,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border
}}
id="Settings"
/>
<DelayLayout type="settings">
@@ -65,9 +82,12 @@ const Home = ({
data={settingsGroups}
keyExtractor={keyExtractor}
renderItem={renderItem}
contentContainerStyle={{
gap: Spacing.LEVEL_2
}}
/>
</DelayLayout>
</>
</View>
);
};

View File

@@ -42,7 +42,7 @@ export const Settings = () => {
paddingRight: insets.right
}}
>
<ScopedThemeProvider value="list">
<ScopedThemeProvider value="base">
<SettingsStack.Navigator
initialRouteName="SettingsHome"
screenListeners={{

File diff suppressed because it is too large Load Diff

View File

@@ -1,232 +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 from "react";
import { CirclePartner, SubscriptionStatus } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import { useState } from "react";
import { useAsync } from "react-async-hook";
import {
ActivityIndicator,
Image,
ScrollView,
TouchableOpacity,
View
} from "react-native";
import { db } from "../../common/database";
import AppIcon from "../../components/ui/AppIcon";
import { Button } from "../../components/ui/button";
import { Notice } from "../../components/ui/notice";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import { useUserStore } from "../../stores/use-user-store";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { openLinkInBrowser } from "../../utils/functions";
import { Pressable } from "../../components/ui/pressable";
export const NotesnookCircle = () => {
const user = useUserStore((state) => state.user);
const isOnTrial =
PremiumService.get() &&
user?.subscription?.status === SubscriptionStatus.TRIAL;
const isFree = !PremiumService.get();
const partners = useAsync(db.circle.partners, []);
return (
<ScrollView
contentContainerStyle={{
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP,
paddingTop: DefaultAppStyles.GAP_VERTICAL
}}
>
{!isFree && !isOnTrial ? null : (
<View>
<Paragraph>
{isFree
? strings.freeUserCircleNotice()
: strings.trialUserCircleNotice()}
</Paragraph>
{!isOnTrial ? null : (
<Button
title={strings.upgradePlan()}
onPress={() => {
Navigation.navigate("PayWall", {
canGoBack: true,
context: useUserStore.getState().user
? "logged-in"
: "logged-out"
});
}}
style={{
alignSelf: "flex-start",
paddingHorizontal: 0
}}
/>
)}
</View>
)}
{partners.loading ? <ActivityIndicator /> : null}
{partners.error ? (
<Notice type="alert" text={partners.error.message} />
) : null}
{partners.result?.map((item) => (
<Partner key={item.id} item={item} available={!isFree && !isOnTrial} />
))}
</ScrollView>
);
};
const Partner = ({
item,
available
}: {
item: CirclePartner;
available: boolean;
}) => {
const { colors } = useThemeColors();
const [code, setCode] = useState<string>();
return (
<View
style={{
borderRadius: defaultBorderRadius,
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading>{item.name}</Heading>
<Image
src={item.logoBase64}
style={{
width: 40,
height: 40
}}
/>
</View>
<Paragraph style={{ textAlign: "justify" }}>
{item.longDescription.trim()}
</Paragraph>
<Paragraph
style={{
alignSelf: "center"
}}
color={colors.primary.accent}
>
{item.offerDescription}
</Paragraph>
{available ? (
<>
{!code ? (
<Button
type="accent"
title={strings.redeemCode()}
width="100%"
onPress={() => {
if (!PremiumService.get()) {
Navigation.navigate("PayWall", {
canGoBack: true,
context: useUserStore.getState().user
? "logged-in"
: "logged-out"
});
return;
}
db.circle
.redeem(item.id)
.then((result) => setCode(result?.code))
.catch((e) => ToastManager.error(e));
}}
/>
) : (
<>
<TouchableOpacity
style={{
backgroundColor: colors.secondary.background,
borderRadius: defaultBorderRadius,
alignItems: "center",
justifyContent: "center",
padding: DefaultAppStyles.GAP_SMALL,
borderWidth: 0.5,
borderColor: colors.secondary.border,
flexDirection: "row",
gap: DefaultAppStyles.GAP_SMALL
}}
activeOpacity={0.9}
onPress={() => {
Clipboard.setString(code);
}}
>
<Paragraph
size={AppFontSize.lg}
color={colors.secondary.paragraph}
>
{code}
</Paragraph>
<AppIcon name="content-copy" />
</TouchableOpacity>
{item.codeRedeemUrl ? (
<Pressable
onPress={() => {
if (item.codeRedeemUrl) {
openLinkInBrowser(
item.codeRedeemUrl.replace("{{code}}", code)
);
}
}}
>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xxs}
>
{strings.clickToDirectlyClaimPromo()}
</Paragraph>
</Pressable>
) : null}
</>
)}
</>
) : null}
</View>
);
};

View File

@@ -1,176 +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 { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { View } from "react-native";
import { Menu, MenuItem } from "react-native-material-menu";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { Dialog } from "../../../components/dialog";
import { Pressable } from "../../../components/ui/pressable";
import Paragraph from "../../../components/ui/typography/paragraph";
import { getColorLinearShade } from "../../../utils/colors";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { verifyUser } from "../functions";
interface PickerOptions<T, B = any> {
getValue: () => B;
updateValue: (item: T) => Promise<void>;
formatValue: (item: T) => any;
compareValue: (current: B, item: T) => boolean;
getItemKey: (item: T) => string;
options: T[];
isFeatureAvailable: () => Promise<boolean>;
isOptionAvailable: (item: T) => Promise<boolean>;
requiresVerification?: () => boolean;
onVerify?: () => Promise<boolean>;
}
export function SettingsPicker<T>({
getValue,
updateValue,
formatValue,
compareValue,
options,
getItemKey,
isFeatureAvailable,
isOptionAvailable,
requiresVerification = () => false,
onVerify
}: PickerOptions<T>) {
const { colors, isDark } = useThemeColors("contextMenu");
const menuRef = useRef<any>(null);
const [width, setWidth] = useState(0);
const [currentValue, setCurrentValue] = useState(getValue());
const onChange = async (item: T) => {
if ((await isFeatureAvailable()) && (await isOptionAvailable(item))) {
menuRef.current?.hide();
await updateValue(item);
setCurrentValue(item);
return;
}
menuRef.current?.hide();
await updateValue(item);
setCurrentValue(item);
};
return (
<View
onLayout={(event) => {
setWidth(event.nativeEvent.layout.width);
}}
style={{
width: "100%"
}}
>
<Menu
ref={menuRef}
animationDuration={200}
style={{
borderRadius: defaultBorderRadius,
backgroundColor: colors.primary.background,
width: width,
marginTop: 60,
overflow: "hidden",
borderWidth: 0.7,
borderColor: getColorLinearShade(
colors.primary.background,
0.07,
isDark
)
}}
onRequestClose={() => {
menuRef.current?.hide();
}}
anchor={
<Pressable
onPress={async () => {
if (
(onVerify && !(await onVerify())) ||
!(await isFeatureAvailable())
)
return;
menuRef.current?.show();
}}
type="secondary"
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
justifyContent: "space-between",
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph>{formatValue(currentValue)}</Paragraph>
<Icon
color={colors.primary.icon}
name="menu-down"
size={AppFontSize.md}
/>
</Pressable>
}
>
<Dialog context="local" />
{options.map((item) => (
<MenuItem
key={getItemKey(item)}
onPress={async () => {
if (requiresVerification?.()) {
verifyUser("local", () => {
onChange(item);
});
} else {
onChange(item);
}
}}
pressColor={colors.primary.hover}
style={{
backgroundColor: compareValue(currentValue, item)
? colors.selected.background
: "transparent",
width: "100%",
maxWidth: width
}}
textStyle={{
fontSize: AppFontSize.sm,
fontFamily: "Inter-Regular",
color: compareValue(currentValue, item)
? colors.primary.accent
: colors.primary.paragraph
}}
>
{formatValue(item)}
</MenuItem>
))}
</Menu>
</View>
);
}
export function createSettingsPicker<T, B>(props: PickerOptions<T, B>) {
const Selector = () => {
return <SettingsPicker {...props} />;
};
return Selector;
}

View File

@@ -22,9 +22,8 @@ import { formatBytes, getFormattedDate } from "@notesnook/common";
import { LegacyBackupFile } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { keepLocalCopy, pick } from "@react-native-documents/picker";
import React, { useEffect, useState } from "react";
import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
import { ActivityIndicator, Platform, View } from "react-native";
import RNFetchBlob, { ReactNativeBlobUtilStat } from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { unzip } from "react-native-zip-archive";
@@ -39,16 +38,15 @@ import {
updateProgress
} from "../../../components/dialogs/progress";
import { Button } from "../../../components/ui/button";
import Heading from "../../../components/ui/typography/heading";
import Paragraph from "../../../components/ui/typography/paragraph";
import { SectionItem } from "../../../screens/settings/section-item";
import { ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SettingsService from "../../../services/settings";
import { refreshAllStores } from "../../../stores/create-db-collection-store";
import { useUserStore } from "../../../stores/use-user-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import Heading from "../../../components/ui/typography/heading";
import { Spacing } from "../../../common/design/spacing";
type PasswordOrKey = { password?: string; encryptionKey?: string };
@@ -84,7 +82,7 @@ const withPassword = () => {
});
};
const restoreBackup = async (options: {
export const restoreBackup = async (options: {
uri: string;
deleteFile?: boolean;
}) => {
@@ -103,6 +101,7 @@ const restoreBackup = async (options: {
startProgress({
title: strings.restoring(),
paragraph: strings.preparingBackupRestore(),
icon: "arrows-clockwise",
canHideProgress: false
});
@@ -292,8 +291,9 @@ export const RestoreBackup = () => {
BACKUP_FILES_CACHE
);
const [loading, setLoading] = useState(true);
const [backupDirectoryAndroid, setBackupDirectoryAndroid] =
useState<ScopedStorage.FileType>();
const backupDirectoryAndroid = useSettingStore(
(state) => state.settings.backupDirectoryAndroid
);
useEffect(() => {
setTimeout(() => {
@@ -305,10 +305,8 @@ export const RestoreBackup = () => {
try {
let files: (ReactNativeBlobUtilStat | ScopedStorage.FileType)[] = [];
if (Platform.OS === "android") {
const backupDirectory = SettingsService.get().backupDirectoryAndroid;
if (backupDirectory) {
setBackupDirectoryAndroid(backupDirectory);
files = await ScopedStorage.listFiles(backupDirectory.uri);
if (backupDirectoryAndroid) {
files = await ScopedStorage.listFiles(backupDirectoryAndroid.uri);
} else {
setLoading(false);
return;
@@ -355,148 +353,72 @@ export const RestoreBackup = () => {
return (
<>
<ScrollView
<LegendList
ListHeaderComponent={
<Heading
style={{
marginBottom: Spacing.LEVEL_2
}}
color={colors.secondary.paragraph}
size={AppFontSize.sm}
fontFamily="MEDIUM"
>
{strings.recentBackups()}
</Heading>
}
ListEmptyComponent={
loading ? (
<View
style={{
justifyContent: "center",
alignItems: "center",
height: 300,
paddingHorizontal: 50
}}
>
<ActivityIndicator
color={colors.primary.accent}
size={AppFontSize.lg}
/>
</View>
) : (
<View
style={{
justifyContent: "center",
alignItems: "center",
gap: 12,
height: 300,
paddingHorizontal: 50
}}
>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
>
{strings.noBackupsFound()}.
</Paragraph>
</View>
)
}
keyExtractor={(item) =>
(item as ScopedStorage.FileType).name ||
(item as ReactNativeBlobUtilStat).filename
}
ListFooterComponent={
<View
style={{
height: 200
}}
/>
}
style={{
width: "100%"
}}
>
<SectionItem
item={{
id: "restore-from-files",
name: strings.restoreFromFiles(),
icon: "folder",
modifer: async () => {
useUserStore.setState({
disableAppLockRequests: true
});
const file = await pick();
const fileCopy = await keepLocalCopy({
destination: "cachesDirectory",
files: [
{
uri: file[0].uri,
fileName: file[0].name ?? `backup_restore_${Date.now()}`
}
]
});
if (fileCopy[0].status === "error") {
ToastManager.error(new Error("File copy error"));
return;
}
setTimeout(() => {
useUserStore.setState({
disableAppLockRequests: false
});
}, 1000);
restoreBackup({
uri: fileCopy[0].localUri,
deleteFile: true
});
},
description: strings.selectBackupFileDesc()
}}
/>
{Platform.OS === "android" ? (
<SectionItem
item={{
id: "select-backup-folder",
name: strings.selectBackupFolder(),
icon: "folder",
modifer: async () => {
const folder = await ScopedStorage.openDocumentTree(true);
let subfolder;
if (folder.name !== "Notesnook backups") {
subfolder = await ScopedStorage.createDirectory(
folder.uri,
"Notesnook backups"
);
} else {
subfolder = folder;
}
SettingsService.set({
backupDirectoryAndroid: subfolder
});
setBackupDirectoryAndroid(subfolder);
setLoading(true);
checkBackups();
},
description: strings.selectFolderForBackupFilesDesc()
}}
/>
) : null}
<LegendList
ListHeaderComponent={
<View
style={{
backgroundColor: colors.primary.background,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Heading color={colors.primary.accent} size={AppFontSize.xs}>
{strings.recentBackups()}
</Heading>
</View>
}
ListEmptyComponent={
loading ? (
<View
style={{
justifyContent: "center",
alignItems: "center",
height: 300,
paddingHorizontal: 50
}}
>
<ActivityIndicator
color={colors.primary.accent}
size={AppFontSize.lg}
/>
</View>
) : (
<View
style={{
justifyContent: "center",
alignItems: "center",
gap: 12,
height: 300,
paddingHorizontal: 50
}}
>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
>
{strings.noBackupsFound()}.
</Paragraph>
</View>
)
}
keyExtractor={(item) =>
(item as ScopedStorage.FileType).name ||
(item as ReactNativeBlobUtilStat).filename
}
ListFooterComponent={
<View
style={{
height: 200
}}
/>
}
style={{
width: "100%"
}}
data={files}
renderItem={renderItem}
/>
</ScrollView>
data={files}
renderItem={renderItem}
/>
</>
);
};
@@ -528,35 +450,36 @@ const BackupItem = ({
width: "100%",
borderRadius: 0,
flexDirection: "row",
borderBottomWidth: 0.5,
borderBottomColor: colors.primary.border,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_SMALL
gap: DefaultAppStyles.GAP_SMALL,
paddingVertical: Spacing.LEVEL_2,
borderBottomWidth: 1,
borderBottomColor: colors.primary.separator
}}
>
<View
style={{
flexShrink: 1
flexShrink: 1,
gap: Spacing.LEVEL_1
}}
>
<Paragraph size={AppFontSize.sm}>{itemName}</Paragraph>
<Heading size={AppFontSize.md}>{itemName}</Heading>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{ width: "100%", maxWidth: "100%" }}
>
Created on {getFormattedDate(item?.lastModified, "date-time")}
{isLegacyBackup ? "(Legacy backup)" : ""} (
{formatBytes((item as ReactNativeBlobUtilStat).size)})
Created: {getFormattedDate(item?.lastModified, "date-time")}
{""}
{formatBytes((item as ReactNativeBlobUtilStat).size)}
</Paragraph>
</View>
<Button
title="Restore"
type="secondaryAccented"
type="plain-outline"
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_1
}}
onPress={() => {
presentDialog({

View File

@@ -17,39 +17,68 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { Spacing } from "../../common/design/spacing";
import Heading from "../../components/ui/typography/heading";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { SectionItem } from "./section-item";
import { SettingSection } from "./types";
import { DefaultAppStyles } from "../../utils/styles";
export const SectionGroup = ({ item }: { item: SettingSection }) => {
export const SectionGroup = ({
item,
isLast
}: {
item: SettingSection;
isLast?: boolean;
}) => {
const { colors } = useThemeColors();
const current = item.useHook && item.useHook();
const isHidden = item.hidden && item.hidden(current);
return isHidden ? null : (
<View
style={{
marginVertical: item.sections ? 10 : 0
}}
>
<View>
{item.name && item.sections ? (
<Heading
style={{
paddingHorizontal: DefaultAppStyles.GAP
paddingHorizontal: Spacing.LEVEL_3,
marginBottom: Spacing.LEVEL_2
}}
color={colors.primary.accent}
size={AppFontSize.xs}
color={colors.secondary.paragraph}
size={AppFontSize.sm}
fontFamily="MEDIUM"
>
{(item.name as string).toUpperCase()}
{item.name as string}
</Heading>
) : null}
{item.sections?.map((item) => (
<SectionItem key={item.name as string} item={item} />
))}
{item.sections?.map((sectionItem, index) =>
sectionItem.type === "group" ? (
<SectionGroup
key={sectionItem.id}
item={sectionItem}
isLast={!item.sections?.[index + 1]}
/>
) : (
<SectionItem key={sectionItem.id as string} item={sectionItem} />
)
)}
{isLast ? null : (
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
width: "100%"
}}
>
<View
style={{
height: 1,
width: "100%",
backgroundColor: colors.primary.border
}}
/>
</View>
)}
</View>
);
};

View File

@@ -17,6 +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 { FeatureResult, useIsFeatureAvailable } from "@notesnook/common";
import { useThemeColors } from "@notesnook/theme";
import {
NavigationProp,
@@ -25,24 +26,22 @@ import {
} from "@react-navigation/native";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, TextInput, View } from "react-native";
import { FeatureResult, useIsFeatureAvailable } from "@notesnook/common";
//@ts-ignore
import ToggleSwitch from "toggle-switch-react-native";
import { FontFamily } from "../../common/design/font";
import { Radius, Spacing } from "../../common/design/spacing";
import PaywallSheet from "../../components/sheets/paywall";
import AppIcon from "../../components/ui/AppIcon";
import { IconButton } from "../../components/ui/icon-button";
import Input from "../../components/ui/input";
import { Pressable } from "../../components/ui/pressable";
import Seperator from "../../components/ui/seperator";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { SettingStore, useSettingStore } from "../../stores/use-setting-store";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { components } from "./components";
import { components } from "./components/components";
import { RouteParams, SettingSection } from "./types";
import { planToDisplayNameShort } from "../../utils/constants";
const _SectionItem = ({ item }: { item: SettingSection }) => {
const { colors } = useThemeColors();
@@ -67,6 +66,15 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
);
const inputRef = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const [inputSelectorValue, setInputSelectorValue] = useState(() =>
item.property
? `${
SettingsService.get()[
item.property as keyof SettingStore["settings"]
] ?? ""
}`
: ""
);
const onChangeSettings = async () => {
if (isDisabled) return;
@@ -93,31 +101,37 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
});
};
const styles =
item.type === "danger"
? {
backgroundColor: colors.error.background
}
: {};
const updateInput = (value: any) => {
inputRef?.current?.setNativeProps({
text: value + ""
});
setInputSelectorValue(`${value}`);
};
const onChangeInputSelectorValue = (text: any) => {
if (text) {
const min = item.minInputValue || 0;
const max = item.maxInputValue || 0;
const value = parseInt(text);
text =
Number.isNaN(value) || value < min ? min : value > max ? max : text;
SettingsService.set({
[item.property as string]: `${text}`
});
const onChangeInputSelectorValue = (text: any, commit?: boolean) => {
// While typing (commit === false) an empty field is allowed so the user can
// clear and retype; on commit (submit/blur) an empty field falls back to min.
if (!text && !commit) {
setInputSelectorValue("");
return;
}
const min = item.minInputValue || 0;
const max = item.maxInputValue || 0;
const value = parseInt(text);
// Always cap the upper bound. Only enforce the lower bound on commit so that
// partial entries (e.g. typing "5" on the way to "50" when min is 8) aren't
// snapped up prematurely.
let clamped: number;
if (Number.isNaN(value)) clamped = min;
else if (value > max) clamped = max;
else if (commit && value < min) clamped = min;
else clamped = value;
// The input is controlled via `value`, so the clamped result is reflected in
// the field directly and can never display a value outside the range.
setInputSelectorValue(`${clamped}`);
SettingsService.set({
[item.property as string]: `${clamped}`
});
};
useEffect(() => {
@@ -138,19 +152,24 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
return true;
}, [isFeatureAvailable]);
const isOn = item.getter
? item.getter(item.property || current)
: settings[item?.property as never];
return isHidden ? null : (
<Pressable
disabled={item.type === "component"}
style={{
width: "100%",
alignItems: "center",
padding: DefaultAppStyles.GAP,
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: DefaultAppStyles.GAP,
borderRadius: 0,
overflow: "hidden",
...styles
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: item.component && !item.name ? 0 : Spacing.LEVEL_1,
borderRadius: Radius.S,
backgroundColor: colors.primary.background,
marginBottom: Spacing.LEVEL_0,
overflow: "hidden"
}}
onPress={async () => {
if (!checkIsFeatureAvailable()) return;
@@ -177,101 +196,160 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
}
}}
>
{!isFeatureAvailable?.isAllowed ? (
<View
style={{
width: 35,
height: 35,
borderRadius: 100,
backgroundColor: colors.primary.accent,
justifyContent: "center",
alignItems: "center",
position: "absolute",
bottom: -8,
right: -8
}}
>
<AppIcon
color={colors.static.orange}
size={AppFontSize.md}
name="crown"
/>
</View>
) : null}
<View
style={{
flexDirection: "row",
flexShrink: 1
flexDirection: "column",
width: "100%"
}}
>
<View
style={{
width: 40,
height: 40,
justifyContent: "center",
flexDirection: "row",
alignItems: "center",
marginRight: 12,
backgroundColor:
item.component === "colorpicker"
? colors.primary.accent
: undefined,
borderRadius: 100
gap: Spacing.LEVEL_2,
width: "100%"
}}
>
{!!item.icon && (
{item.icon ? (
<View
style={{
width: 32,
height: 32,
justifyContent: "center",
alignItems: "center",
backgroundColor:
item.component === "colorpicker"
? colors.primary.accent
: colors.secondary.background,
borderRadius: Radius.XS
}}
>
<AppIcon
color={
item.type === "danger"
? colors.error.accent
: colors.primary.icon
}
iconFamily={item.iconFamily}
name={item.icon}
size={item.iconSize || 16}
/>
</View>
) : null}
<View
style={{
paddingRight: item.type === "switch" ? Spacing.LEVEL_1 : 0,
gap: Spacing.LEVEL_1,
flexShrink: 1,
flex: 1
}}
>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1,
alignItems: "center"
}}
>
{item.name ? (
<Heading
color={colors.primary.heading}
fontSize="MD"
lineHeight="100%"
>
{typeof item.name === "function"
? item.name(current)
: item.name}
</Heading>
) : null}
{!isFeatureAvailable?.isAllowed ? (
<View
style={{
paddingVertical: Spacing.LEVEL_0 / 2,
paddingHorizontal: Spacing.LEVEL_1,
borderRadius: 100,
backgroundColor: colors.primary.accent,
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
gap: Spacing.LEVEL_0
}}
>
<AppIcon
color={colors.static.orange}
size={AppFontSize.md}
name="crown"
/>
{isFeatureAvailable?.availableOn ? (
<Paragraph
style={{ color: colors.primary.accentForeground }}
fontSize="XXS"
fontFamily="MEDIUM"
>
{planToDisplayNameShort(isFeatureAvailable?.availableOn)}
</Paragraph>
) : null}
</View>
) : null}
</View>
{item.description ? (
<Paragraph
color={colors.primary.paragraph}
fontSize="SM"
lineHeight="100%"
>
{typeof item.description === "function"
? item.description(current)
: item.description}
</Paragraph>
) : null}
</View>
{item.type === "switch" && !loading && (
<AppIcon
name={isOn ? "toggle-on" : "toggle-off"}
iconFamily="notesnook"
size={16}
color={
item.type === "danger"
? colors.error.icon
: colors.secondary.icon
isOn
? [colors.primary.accent, colors.primary.background]
: [colors.disabled.icon, colors.primary.background]
}
iconFamily={item.iconFamily}
name={item.icon}
size={item.iconSize || 30}
/>
)}
{item.type === "screen" ? (
<AppIcon
name="chevron-right"
iconFamily="notesnook"
size={16}
color={colors.secondary.paragraph}
/>
) : null}
{loading ? (
<ActivityIndicator size={16} color={colors.primary.accent} />
) : null}
</View>
<View
style={{
flexShrink: 1,
paddingRight: item.type === "switch" ? 10 : 0,
flex: item.type === "component" ? 1 : 0
gap: Spacing.LEVEL_1,
paddingLeft: item.name ? Spacing.LEVEL_2 + 32 : undefined
}}
>
{item.name ? (
<Heading
color={
item.type === "danger"
? colors.error.paragraph
: colors.primary.heading
}
size={AppFontSize.sm}
>
{typeof item.name === "function" ? item.name(current) : item.name}
</Heading>
) : null}
{!!item.description && (
<Paragraph
color={
item.type === "danger"
? colors.error.paragraph
: colors.primary.paragraph
}
size={AppFontSize.sm}
>
{typeof item.description === "function"
? item.description(current)
: item.description}
</Paragraph>
)}
{!!item.component && item.type !== "screen" && (
<>
<Seperator half />
<View
style={{
width: "100%",
paddingTop: item.icon ? Spacing.LEVEL_2 : 0
}}
>
{components[item.component]}
</>
</View>
)}
{item.type === "input" && (
@@ -290,7 +368,15 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
});
item.inputProperties?.onSubmitEditing?.(text as any);
}}
containerStyle={{ marginTop: DefaultAppStyles.GAP_VERTICAL }}
containerStyle={{
marginTop: Spacing.LEVEL_2,
backgroundColor: colors.secondary.background,
borderWidth: 0
}}
inputStyle={{
color: colors.primary.heading
}}
fontSize={AppFontSize.sm}
fwdRef={inputRef}
onLayout={() => {
inputRef?.current?.setNativeProps({
@@ -309,11 +395,111 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
style={{
flexDirection: "row",
alignItems: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL
marginTop: Spacing.LEVEL_2,
backgroundColor: colors.secondary.background,
alignSelf: "flex-start",
padding: Spacing.LEVEL_2,
gap: Spacing.LEVEL_1,
borderRadius: Radius.S
}}
>
<IconButton
name="plus"
color={colors.primary.icon}
iconFamily="notesnook"
onPress={() => {
if (!checkIsFeatureAvailable()) return;
if (isDisabled) return;
const rawValue = SettingsService.get()[
item.property as keyof SettingStore["settings"]
] as string;
if (rawValue) {
const currentValue = parseInt(rawValue);
const max = item.maxInputValue || 0;
if (currentValue >= max) return;
const nextValue = currentValue + 1;
SettingsService.set({
[item.property as string]: nextValue
});
updateInput(nextValue);
}
}}
size={16}
type="tertiary"
style={{
borderRadius: Radius.XXS,
padding: Spacing.LEVEL_0,
width: undefined,
height: undefined
}}
/>
<View
style={{
flexDirection: "row",
alignItems: "flex-end",
gap: 1
}}
>
<Input
{...item.inputProperties}
onSubmit={(e) => {
onChangeInputSelectorValue(e.nativeEvent.text, true);
item.inputProperties?.onSubmitEditing?.(e);
}}
editable={!isDisabled}
value={inputSelectorValue}
onChangeText={(text) => {
onChangeInputSelectorValue(text);
item.inputProperties?.onSubmitEditing?.(text as any);
}}
keyboardType="decimal-pad"
containerStyle={{
borderWidth: 0,
paddingLeft: 0,
paddingRight: 0,
height: 25,
borderRadius: 0,
minWidth: 25
}}
fontSize={AppFontSize.sm}
inputStyle={{
textAlign: "center",
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
fontFamily: FontFamily.SEMI_BOLD,
color: colors.primary.heading
}}
wrapperStyle={{
flexGrow: 0,
marginBottom: 0,
paddingLeft: 0,
paddingRight: 0
}}
buttons={
<>
{item.inputBadgeValue ? (
<Paragraph
fontSize="XXS"
style={{
marginLeft: 1,
marginTop: 2,
color: colors.primary.heading
}}
color={colors.primary.paragraph}
>
px
</Paragraph>
) : null}
</>
}
/>
</View>
<IconButton
name="minus"
iconFamily="notesnook"
color={colors.primary.icon}
onPress={() => {
if (!checkIsFeatureAvailable()) return;
@@ -332,69 +518,21 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
updateInput(nextValue);
}
}}
size={AppFontSize.xl}
/>
<Input
{...item.inputProperties}
onSubmit={(e) => {
onChangeInputSelectorValue(e.nativeEvent.text);
item.inputProperties?.onSubmitEditing?.(e);
size={16}
type="tertiary"
style={{
borderRadius: Radius.XXS,
padding: Spacing.LEVEL_0,
width: undefined,
height: undefined
}}
editable={!isDisabled}
onChangeText={(text) => {
onChangeInputSelectorValue(text);
item.inputProperties?.onSubmitEditing?.(text as any);
}}
keyboardType="decimal-pad"
containerStyle={{
width: 60
}}
inputStyle={{
width: 60,
textAlign: "center"
}}
wrapperStyle={{
maxWidth: 60,
flexGrow: 0,
marginBottom: 0,
marginHorizontal: DefaultAppStyles.GAP_SMALL
}}
fwdRef={inputRef}
onLayout={() => {
if (item.property) {
updateInput(SettingsService.get()[item.property]);
}
}}
defaultValue={item.inputProperties?.defaultValue}
/>
<IconButton
name="plus"
color={colors.primary.icon}
onPress={() => {
if (!checkIsFeatureAvailable()) return;
if (isDisabled) return;
const rawValue = SettingsService.get()[
item.property as keyof SettingStore["settings"]
] as string;
if (rawValue) {
const currentValue = parseInt(rawValue);
const max = item.maxInputValue || 0;
if (currentValue >= max) return;
const nextValue = currentValue + 1;
SettingsService.set({
[item.property as string]: nextValue
});
updateInput(nextValue);
}
}}
size={AppFontSize.xl}
/>
</View>
)}
</View>
</View>
{item.type === "switch" && !loading && (
{/* {item.type === "switch" && !loading && (
<ToggleSwitch
isOn={
item.getter
@@ -407,14 +545,7 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
animationSpeed={150}
onToggle={onChangeSettings}
/>
)}
{loading ? (
<ActivityIndicator
size={AppFontSize.xxl}
color={colors.primary.accent}
/>
) : null}
)} */}
</Pressable>
);
};

View File

@@ -1,238 +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 { isServerCompatible } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { View } from "react-native";
import { presentDialog } from "../../components/dialog/functions";
import { Button } from "../../components/ui/button";
import Input from "../../components/ui/input";
import { Notice } from "../../components/ui/notice";
import Paragraph from "../../components/ui/typography/paragraph";
import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { HostId, HostIds } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
import { DefaultAppStyles } from "../../utils/styles";
export const ServerIds = [
"notesnook-sync",
"auth",
"sse",
"monograph"
] as const;
export type ServerId = (typeof ServerIds)[number];
type Server = {
id: ServerId;
host: HostId;
title: string;
example: string;
description: string;
versionEndpoint: string;
};
type VersionResponse = {
version: number;
id: string;
instance: string;
};
const SERVERS: Server[] = [
{
id: "notesnook-sync",
host: "API_HOST",
title: strings.syncServer(),
example: "http://localhost:4326",
description: strings.syncServerDesc(),
versionEndpoint: "/version"
},
{
id: "auth",
host: "AUTH_HOST",
title: strings.authServer(),
example: "http://localhost:5326",
description: strings.authServerDesc(),
versionEndpoint: "/version"
},
{
id: "sse",
host: "SSE_HOST",
title: strings.sseServer(),
example: "http://localhost:7326",
description: strings.sseServerDesc(),
versionEndpoint: "/version"
},
{
id: "monograph",
host: "MONOGRAPH_HOST",
title: strings.monographServer(),
example: "http://localhost:6326",
description: strings.monographServerDesc(),
versionEndpoint: "/api/version"
}
];
export function ServersConfiguration() {
const { colors } = useThemeColors();
const [error, setError] = useState<string>();
const [success, setSuccess] = useState<boolean>();
const [urls, setUrls] = useState<Partial<Record<HostId, string>>>(
SettingsService.getProperty("serverUrls") || {}
);
const isLoggedIn = useUserStore((state) => !!state.user);
return (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
gap: 12,
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
{isLoggedIn ? (
<Notice text={strings.logoutToChangeServerUrls()} type="alert" />
) : null}
<View style={{ flexDirection: "column" }}>
{SERVERS.map((server) => (
<Input
key={server.id}
editable={!isLoggedIn}
placeholder={`${server.id} e.g. ${server.example}`}
validationType="url"
defaultValue={urls[server.host]}
errorMessage={strings.enterValidUrl()}
onChangeText={(value) =>
setUrls((s) => {
s[server.host] = value;
return s;
})
}
/>
))}
{error ? (
<Paragraph
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
color={colors.error.paragraph}
>
{error}
</Paragraph>
) : null}
{success === true ? (
<Paragraph
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
color={colors.success.paragraph}
>
{strings.connectedToServer()}
</Paragraph>
) : null}
<View
style={{
marginTop: 1,
justifyContent: "flex-end",
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Button
disabled={isLoggedIn}
type="secondary"
width="100%"
onPress={async () => {
setError(undefined);
try {
for (const host of HostIds) {
const url = urls[host];
const server = SERVERS.find((s) => s.host === host)!;
if (!server) throw new Error(strings.serverNotFound(host));
if (!url) throw new Error(strings.allServerUrlsRequired());
const version = await fetch(`${url}${server.versionEndpoint}`)
.then((r) => r.json() as Promise<VersionResponse>)
.catch(() => undefined);
if (!version)
throw new Error(
`${strings.couldNotConnectTo(server.title)}`
);
if (version.id !== server.id)
throw new Error(
`${strings.incorrectServerUrl(url, server.title)}.`
);
if (!isServerCompatible(version.version)) {
throw new Error(
strings.serverVersionMismatch(server.title, url)
);
}
}
setSuccess(true);
} catch (e) {
setError((e as Error).message);
}
}}
title={strings.testConnection()}
/>
<Button
type="accent"
disabled={isLoggedIn}
width="100%"
onPress={async () => {
if (!success) {
ToastManager.show({
heading: strings.testConnectionBeforeSave()
});
return;
}
SettingsService.setProperty(
"serverUrls",
urls as Record<HostId, string>
);
presentDialog({
title: strings.serverUrlChanged(),
paragraph: strings.restartAppToTakeEffect(),
negativeText: strings.done()
});
}}
title={strings.save()}
/>
<Button
disabled={isLoggedIn}
type="error"
width="100%"
title={strings.resetServerUrls()}
onPress={async () => {
if (isLoggedIn) return;
SettingsService.setProperty("serverUrls", undefined);
presentDialog({
title: strings.serverUrlsReset(),
paragraph: strings.restartAppToTakeEffect(),
negativeText: strings.done()
});
}}
/>
</View>
</View>
</View>
);
}

File diff suppressed because it is too large Load Diff

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