mobile: update 2fa sheets design

This commit is contained in:
Ammar Ahmed
2026-06-12 12:02:42 +05:00
parent 9c5cf1a5ec
commit 5fadb95ddd
11 changed files with 597 additions and 359 deletions

View File

@@ -19,7 +19,7 @@ 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 { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import isMobilePhone from "validator/lib/isMobilePhone";
import React, {
@@ -37,20 +37,20 @@ import {
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 { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import DialogHeader from "../../../components/dialog/dialog-header";
import AppIcon from "../../../components/ui/AppIcon";
import { Button } from "../../../components/ui/button";
import { IconButton } from "../../../components/ui/icon-button";
import FormInput, {
createFormRef,
validators
} from "../../../components/ui/input/form-input";
import PinInput from "../../../components/ui/pin-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";
@@ -63,27 +63,25 @@ 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
icon: "device-mobile-camera"
},
{
id: "sms",
title: strings.mfaSmsTitle(),
body: strings.mfaSmsDesc(),
icon: "message-plus-outline"
icon: "chat"
},
{
id: "email",
title: strings.mfaEmailTitle(),
body: strings.mfaEmailDesc(),
icon: "email-outline"
icon: "envelope-simple"
}
];
type MFAMethod = {
@@ -116,67 +114,78 @@ export const MFAMethodsPickerStep = ({ recovery, onSuccess }: MFAStepProps) => {
return mfaMethods.filter((m) => m.id !== user?.mfa?.primaryMethod);
};
const onMethodPress = (item: MFAMethod) => {
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);
};
return (
<>
<DialogHeader
title={strings.twoFactorAuth()}
paragraph={strings.twoFactorAuthDesc()}
padding={12}
style={{
paddingTop: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_3
}}
/>
<Seperator />
{getMethods().map((item) => (
{getMethods().map((item, index, methods) => (
<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);
}}
onPress={() => onMethodPress(item)}
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
marginTop: 0,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_1,
marginBottom: index === methods.length - 1 ? 0 : Spacing.LEVEL_0,
flexDirection: "row",
borderRadius: 0,
alignItems: "flex-start"
borderRadius: Radius.S,
alignItems: "center",
backgroundColor: colors.primary.background
}}
>
{item.icon && (
<IconButton
type="secondary"
<View
style={{
width: 50,
height: 50,
marginRight: 10
width: Spacing.LEVEL_7,
height: Spacing.LEVEL_7,
borderRadius: Radius.XS,
marginRight: Spacing.LEVEL_1,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
size={20}
color={
item.recommended ? colors.primary.accent : colors.primary.icon
}
name={item.icon}
/>
>
<AppIcon
size={16}
iconFamily="notesnook"
color={colors.primary.icon}
name={item.icon}
/>
</View>
)}
<View
style={{
flexShrink: 1
flex: 1,
flexShrink: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading size={AppFontSize.md}>{item.title}</Heading>
<Heading size={AppFontSize.md} lineHeight="100%">
{item.title}
</Heading>
<Paragraph size={AppFontSize.sm}>{item.body}</Paragraph>
</View>
</Pressable>
@@ -211,6 +220,7 @@ export const MFASetup = ({
const [loading, setLoading] = useState(method?.id === "app" ? true : false);
const [enabling, setEnabling] = useState(false);
const [sending, setSending] = useState(false);
const [codeValue, setCodeValue] = useState("");
const [generalError, setGeneralError] = useState<string>();
useEffect(() => {
@@ -245,6 +255,7 @@ export const MFASetup = ({
: formRef.current.getValue("target")
);
formRef.current.setValue("code", "");
setCodeValue("");
setGeneralError(undefined);
}, [authenticatorDetails.sharedKey, methodId, user?.email]);
@@ -280,11 +291,18 @@ export const MFASetup = ({
];
const onNext = async () => {
if (formRef.current.validateField("code")) return;
const code = codeValue.trim();
const codeValidationError = codeValidators
.map((validator) => validator(code))
.find(Boolean);
if (codeValidationError) {
setGeneralError(codeValidationError);
return;
}
try {
if (!method) return;
const code = formRef.current.getValue("code").trim();
setGeneralError(undefined);
setEnabling(true);
@@ -300,7 +318,7 @@ export const MFASetup = ({
setEnabling(false);
} catch (e) {
const error = e as Error;
formRef.current.setError("code", error.message);
setGeneralError(error.message);
setEnabling(false);
}
};
@@ -363,18 +381,34 @@ export const MFASetup = ({
}
};
const onChangeMethodPress = () => {
setStep &&
setStep({
id: "mfapick",
props: {
recovery: recovery
}
});
};
return !method ? null : (
<View>
<View
style={{
gap: Spacing.LEVEL_3
}}
>
<DialogHeader
title={method?.title}
paragraph={method?.body}
padding={12}
style={{
paddingTop: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_3
}}
/>
<Seperator />
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
paddingHorizontal: Spacing.LEVEL_3
}}
>
{loading ? (
@@ -423,16 +457,27 @@ export const MFASetup = ({
}
onSubmitEditing={onSendCode}
validators={targetValidators}
containerStyle={{
borderWidth: 0,
borderRadius: Radius.XS
}}
keyboardType={
method.id === "email" ? "email-address" : "phone-pad"
}
buttons={
<Button
onPress={onSendCode}
type="transparent"
fontFamily="MEDIUM"
fontSize={AppFontSize.sm}
buttonType={{
text: colors.primary.accent
}}
loading={sending}
style={{
paddingVertical: 0,
paddingHorizontal: 0
paddingHorizontal: 0,
marginLeft: Spacing.LEVEL_1
}}
title={
sending
@@ -449,42 +494,67 @@ export const MFASetup = ({
}
/>
<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={() => {
<View
style={{
height: 1,
marginVertical: Spacing.LEVEL_3,
backgroundColor: colors.primary.border
}}
/>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1,
paddingVertical: Spacing.LEVEL_1,
marginBottom: Spacing.LEVEL_3
}}
>
<View
style={{
width: Spacing.LEVEL_7,
height: Spacing.LEVEL_7,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
size={16}
iconFamily="notesnook"
color={colors.primary.icon}
name="device-mobile-camera"
/>
</View>
<View
style={{
flex: 1,
gap: Spacing.LEVEL_1
}}
>
<Heading size={AppFontSize.md} lineHeight="100%">
{strings.enterSixDigitCode()}
</Heading>
<Paragraph size={AppFontSize.sm}>
{codeHelpText[method?.id]}
</Paragraph>
</View>
</View>
<PinInput
value={codeValue}
length={6}
inputRef={codeInputRef}
onSubmitEditing={onNext}
onChangeText={(value) => {
formRef.current.setValue("code", value);
setCodeValue(value);
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"
}}
sanitize={(value) => value.replace(/\D/g, "")}
/>
{generalError ? (
@@ -492,7 +562,7 @@ export const MFASetup = ({
size={AppFontSize.sm}
style={{
color: colors.error.icon,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
marginBottom: Spacing.LEVEL_2,
textAlign: "center",
width: "100%"
}}
@@ -501,33 +571,30 @@ export const MFASetup = ({
</Paragraph>
) : null}
<Seperator />
<Button
title={enabling ? null : strings.next()}
type="accent"
width={250}
onPress={onNext}
loading={enabling}
<View
style={{
borderRadius: 100,
marginBottom: DefaultAppStyles.GAP_VERTICAL
gap: Spacing.LEVEL_2,
marginTop: Spacing.LEVEL_4
}}
/>
>
<Button
title={enabling ? null : strings.next()}
type="accent"
width="100%"
onPress={onNext}
loading={enabling}
style={{
borderRadius: Radius.S
}}
/>
<Button
title={strings.change2faMethod()}
type="plain"
height={25}
onPress={() => {
setStep &&
setStep({
id: "mfapick",
props: {
recovery: recovery
}
});
}}
/>
<Button
title={strings.change2faMethod()}
type="plain-outline"
width="100%"
onPress={onChangeMethodPress}
/>
</View>
</>
)}
</View>
@@ -544,6 +611,11 @@ export const MFARecoveryCodes = ({
const [codes, setCodes] = useState([]);
const [loading, setLoading] = useState(true);
const codeRows = [] as string[][];
for (let i = 0; i < codes.length; i += 3) {
codeRows.push(codes.slice(i, i + 3));
}
useEffect(() => {
(async () => {
try {
@@ -558,140 +630,208 @@ export const MFARecoveryCodes = ({
})();
}, []);
const onCopyCodesPress = () => {
const codeString = codes.join("\n");
Clipboard.setString(codeString);
ToastManager.show({
heading: strings.codesCopied(),
type: "success",
context: "local"
});
};
const onSaveToFilePress = 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);
}
};
const onCompletePress = () => {
if (isSetup) {
onSuccess && onSuccess(method);
} else {
eSendEvent(eCloseSheet);
}
};
return (
<View>
<View
style={{
gap: Spacing.LEVEL_3
}}
>
<DialogHeader
centered={true}
title={strings.saveRecoveryCodes()}
paragraph={strings.saveRecoveryCodesDesc()}
padding={12}
style={{
paddingTop: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_3
}}
/>
<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={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
{loading ? (
<View
style={{
flexDirection: "row",
justifyContent: "center",
marginBottom: DefaultAppStyles.GAP_VERTICAL
alignItems: "center",
width: "100%",
marginBottom: 50
}}
>
<Button
title={strings.copyCodes()}
onPress={() => {
const codeString = codes.join("\n");
Clipboard.setString(codeString);
ToastManager.show({
heading: strings.codesCopied(),
type: "success",
context: "local"
});
}}
<ActivityIndicator
color={colors.primary.accent}
style={{
marginRight: 10
height: 50
}}
/>
<Paragraph>
{strings.gettingRecoveryCodes()}... {strings.pleaseWait()}
</Paragraph>
</View>
) : (
<>
<View
style={{
backgroundColor: colors.secondary.background,
borderRadius: Radius.S,
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_3,
gap: Spacing.LEVEL_1
}}
>
{codeRows.map((row, rowIndex) => (
<View
key={`row-${rowIndex}`}
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2
}}
>
{row.map((code, colIndex) => (
<Paragraph
key={`${rowIndex}-${colIndex}`}
size={AppFontSize.xs}
style={{
flex: 1,
color: colors.primary.heading
}}
>
{code}
</Paragraph>
))}
{row.length < 3
? Array.from({ length: 3 - row.length }).map((_, idx) => (
<View
key={`empty-${rowIndex}-${idx}`}
style={{ flex: 1 }}
/>
))
: null}
</View>
))}
<View
style={{
height: 1,
marginTop: Spacing.LEVEL_1,
backgroundColor: colors.primary.border
}}
/>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2,
marginTop: Spacing.LEVEL_1
}}
>
<Button
title={strings.copyCodes()}
type="transparent"
fontFamily="SEMI_BOLD"
fontSize={AppFontSize.xs}
icon="content-copy"
iconSize={14}
iconColor={colors.primary.accent}
buttonType={{ text: colors.primary.accent }}
onPress={onCopyCodesPress}
style={{
flex: 1,
borderRadius: Radius.S,
paddingVertical: Spacing.LEVEL_1,
paddingHorizontal: Spacing.LEVEL_1
}}
/>
<Button
title={strings.saveToFile()}
type="transparent"
fontFamily="SEMI_BOLD"
fontSize={AppFontSize.xs}
icon="download-simple"
iconFamily="notesnook"
iconSize={14}
iconColor={colors.primary.accent}
buttonType={{ text: colors.primary.accent }}
onPress={onSaveToFilePress}
style={{
flex: 1,
paddingVertical: Spacing.LEVEL_1,
paddingHorizontal: Spacing.LEVEL_1,
borderRadius: Radius.S
}}
/>
</View>
</View>
<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);
}
title={isSetup ? strings.next() : strings.done()}
type="accent"
width="100%"
onPress={onCompletePress}
style={{
borderRadius: Radius.S,
marginTop: Spacing.LEVEL_2,
marginBottom: Spacing.LEVEL_2
}}
/>
</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>
</View>
);
};
@@ -702,61 +842,120 @@ MFARecoveryCodes.present = (methodId: MFAMethod["id"]) => {
});
};
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();
const { colors, isDark } = useThemeColors();
const onDonePress = () => {
eSendEvent(eCloseSheet);
};
const onSecondaryMethodPress = () => {
MFASheet.present(true);
};
return (
<View
style={{
alignItems: "center"
gap: Spacing.LEVEL_4
}}
>
<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);
}}
<View
style={{
borderRadius: 100,
marginBottom: DefaultAppStyles.GAP_VERTICAL
alignItems: "center",
justifyContent: "center",
marginTop: Spacing.LEVEL_4
}}
/>
>
<View
style={{
width: 93,
height: 93,
borderRadius: 46.5,
alignItems: "center",
justifyContent: "center",
backgroundColor: isDark ? "#0A572920" : "#00883614"
}}
>
<View
style={{
width: 80,
height: 80,
borderRadius: 40,
alignItems: "center",
justifyContent: "center",
backgroundColor: isDark ? "#0A572930" : "#0088361F"
}}
>
<View
style={{
width: 47,
height: 47,
borderRadius: 23.5,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.primary.accent
}}
>
<AppIcon
name="check"
size={20}
color={colors.primary.accentForeground}
/>
</View>
</View>
</View>
</View>
{!recovery ? (
<View
style={{
alignItems: "center",
gap: Spacing.LEVEL_1,
paddingHorizontal: Spacing.LEVEL_3
}}
>
<Heading
size={AppFontSize.xl}
lineHeight="100%"
style={{ textAlign: "center" }}
>
{recovery
? strings.fallbackMethodEnabled()
: strings.twoFactorAuthEnabled()}
</Heading>
<Paragraph size={AppFontSize.xs} style={{ textAlign: "center" }}>
{strings.accountIsSecure()}
</Paragraph>
</View>
<View
style={{
width: "100%",
gap: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_3
}}
>
<Button
title={strings.secondary2faMethod()}
type="plain"
height={25}
onPress={() => {
MFASheet.present(true);
title={strings.done()}
type="accent"
width="100%"
onPress={onDonePress}
style={{
borderRadius: Radius.S
}}
/>
) : null}
{!recovery ? (
<Button
title={strings.secondary2faMethod()}
type="plain-outline"
width="100%"
onPress={onSecondaryMethodPress}
style={{
borderRadius: Radius.S
}}
/>
) : null}
</View>
</View>
);
};

View File

@@ -47,9 +47,15 @@ const Group = ({
return false;
}
});
const renderItem = ({ item }: { item: SettingSection; index: number }) =>
const renderItem = ({
item,
index
}: {
item: SettingSection;
index: number;
}) =>
item.type === "group" ? (
<SectionGroup item={item} />
<SectionGroup item={item} isLast={!route.params.sections?.[index + 1]} />
) : (
<SectionItem item={item} />
);

View File

@@ -292,7 +292,6 @@ export const accountGroup: SettingSection = {
name: strings.subscriptionNotActivated(),
icon: "warning-circle",
iconFamily: "notesnook",
isNavigation: true,
hidden: () => Platform.OS !== "ios",
modifer: async () => {
if (Platform.OS === "android") return;
@@ -355,63 +354,77 @@ export const accountGroup: SettingSection = {
iconFamily: "notesnook",
sections: [
{
id: "enable-2fa",
name: strings.change2faMethod(),
modifer: () => {
verifyUser("global", async () => {
MFASheet.present();
});
},
useHook: () => useUserStore((state) => state.user),
description: strings.change2faMethodDesc()
},
{
id: "2fa-fallback",
name: strings.addFallback2faMethod(),
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(),
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");
});
},
useHook: () => useUserStore((state) => state.user),
hidden: (user) => {
return !(user as User)?.mfa?.isEnabled;
},
description: strings.viewRecoveryCodesDesc()
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()
}
]
}
]
},
@@ -419,7 +432,6 @@ export const accountGroup: SettingSection = {
id: "recovery-key",
name: strings.saveDataRecoveryKey(),
iconFamily: "notesnook",
isNavigation: true,
modifer: async () => {
// if (await verifyUser()) {
@@ -686,7 +698,6 @@ export const accountGroup: SettingSection = {
name: strings.forcePullChanges(),
description: strings.forcePullChangesDesc(),
icon: "git-pull-request",
isNavigation: true,
iconFamily: "notesnook",
modifer: () => {
presentDialog({
@@ -711,7 +722,6 @@ export const accountGroup: SettingSection = {
description: strings.forcePushChangesDesc(),
icon: "arrow-fat-up",
iconFamily: "notesnook",
isNavigation: true,
modifer: () => {
presentDialog({
title: strings.forcePushChanges(),

View File

@@ -46,11 +46,17 @@ 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 File

@@ -25,7 +25,13 @@ import Heading from "../../components/ui/typography/heading";
import { AppFontSize } from "../../utils/size";
import { SectionItem } from "./section-item";
import { SettingSection } from "./types";
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);
@@ -49,28 +55,34 @@ export const SectionGroup = ({ item }: { item: SettingSection }) => {
</Heading>
) : null}
{item.sections?.map((item) =>
item.type === "group" ? (
<SectionGroup key={item.id} item={item} />
{item.sections?.map((sectionItem, index) =>
sectionItem.type === "group" ? (
<SectionGroup
key={sectionItem.id}
item={sectionItem}
isLast={!item.sections?.[index + 1]}
/>
) : (
<SectionItem key={item.id as string} item={item} />
<SectionItem key={sectionItem.id as string} item={sectionItem} />
)
)}
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
width: "100%"
}}
>
{isLast ? null : (
<View
style={{
height: 1,
width: "100%",
backgroundColor: colors.primary.border
paddingHorizontal: Spacing.LEVEL_3,
width: "100%"
}}
/>
</View>
>
<View
style={{
height: 1,
width: "100%",
backgroundColor: colors.primary.border
}}
/>
</View>
)}
</View>
);
};

View File

@@ -421,7 +421,7 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
/>
)}
{item.type === "screen" || item.isNavigation ? (
{item.type === "screen" ? (
<AppIcon
name="chevron-right"
iconFamily="notesnook"

View File

@@ -37,7 +37,6 @@ export type SettingSection = {
description?: string | ((current: unknown) => string);
icon?: string;
iconFamily?: IconProps["iconFamily"];
isNavigation?: boolean;
iconSize?: number;
property?: keyof Settings;
sections?: SettingSection[];

View File

@@ -1 +1 @@
{"m":{"f":"notesnook-icons","u":1024,"z":1020,"s":59648,"h":"732c10c1b7a68a314543aa54f89c5cc11a6dcf1d4457e785521a86655a4306c0"},"i":{"archive":[1024,[[59648,"#666666"]]],"arrow-back":[1024,[[59649,"rgba(255,255,255,0.7)"]]],"arrow-counter-clockwise":[1024,[[59650,"var(--fill-0, #181818)"]]],"arrow-fat-up":[1024,[[59651,"var(--fill-0, #181818)"]]],"arrow-u-up-left":[1024,[[59652,"var(--fill-0, #181818)"]]],"arrows-clockwise":[1024,[[59653,"var(--fill-0, #181818)"]]],"bag-simple":[1024,[[59654,"var(--fill-0, #181818)"]]],"bell":[1024,[[59655,"#666666"]]],"book-open":[1024,[[59656,"#666666"]]],"bookmark":[1024,[[59657,"#666666"]]],"box-empty":[1024,[[59658,"#B0B0B1"]]],"bug-droid":[1024,[[59659,"var(--fill-0, #181818)"]]],"calendar":[1024,[[59660,"rgba(102,102,102,0.8)"]]],"chart-line-up":[1024,[[59661,"var(--fill-0, #181818)"]]],"chat":[1024,[[59662,"white"]]],"checkbox":[1024,[[59663,"#008836"],[59664,"white"]]],"checks":[1024,[[59665,"#181818"]]],"chevron-down":[1024,[[59666,"#181818"]]],"chevron-right":[1024,[[59667,"#181818"]]],"chevron-up":[1024,[[59668,"#181818"]]],"clock-counter-clockwise":[1024,[[59669,"var(--fill-0, #181818)"]]],"close":[1024,[[59670,"#666666"]]],"cloud-check":[1024,[[59671,"var(--fill-0, #181818)"]]],"cloud":[1024,[[59672,"white"]]],"dark-mode-outline":[1024,[[59673,"currentColor"]]],"device-mobile-camera":[1024,[[59674,"var(--fill-0, #181818)"]]],"discord-logo":[1024,[[59675,"var(--fill-0, #181818)"]]],"dots-three":[1024,[[59676,"#202020"]]],"download-simple":[1024,[[59677,"var(--fill-0, #181818)"]]],"drive-file-move":[1088,[[59678,"#666666"]]],"edit-pencil":[1024,[[59679,"#181818"]]],"envelope-simple":[1024,[[59680,"white"]]],"export":[1024,[[59681,"var(--fill-0, #181818)"]]],"eye-closed":[1024,[[59682,"white"]]],"eye-open":[1024,[[59683,"currentColor"]]],"eye-slash":[1024,[[59684,"var(--fill-0, #181818)"]]],"file-dashed":[1024,[[59685,"var(--fill-0, #181818)"]]],"file-text":[1024,[[59686,"var(--fill-0, #181818)"]]],"file":[1024,[[59687,"white"]]],"funnel":[1024,[[59688,"#666666"]]],"git-pull-request":[1024,[[59689,"var(--fill-0, #181818)"]]],"github-logo":[1024,[[59690,"var(--fill-0, #181818)"]]],"hard-drives":[1024,[[59691,"var(--fill-0, #181818)"]]],"home":[1024,[[59692,"#181818"]]],"image-outline":[1024,[[59693,"white"]]],"key":[1024,[[59694,"var(--fill-0, #181818)"]]],"link":[560,[[59695,"#666666"]]],"lock-simple":[1024,[[59696,"white"]]],"lock":[1024,[[59697,"#181818"]]],"mastodon-logo":[1024,[[59698,"var(--fill-0, #181818)"]]],"menu":[1024,[[59699,"#181818"]]],"network":[1024,[[59700,"var(--fill-0, #181818)"]]],"note":[1024,[[59701,"#181818"]]],"notification":[1024,[[59702,"var(--fill-0, #181818)"]]],"paperclip":[1024,[[59703,"var(--fill-0, #181818)"]]],"pause":[1024,[[59704,"var(--fill-0, #181818)"]]],"pencil-simple-line":[1024,[[59705,"var(--fill-0, #181818)"]]],"pin":[939,[[59706,"#666666"]]],"plus":[1024,[[59707,"#666666"]]],"recovery-key-cloud-arrow-down":[1024,[[59708,"var(--fill-0, #181818)"]]],"recovery-key-copy":[1024,[[59709,"var(--stroke-0, #008836)"]]],"recovery-key-file":[1024,[[59710,"var(--fill-0, #181818)"]]],"recovery-key-key":[1024,[[59711,"var(--fill-0, #181818)"]]],"recovery-key-qr-code":[1024,[[59712,"var(--fill-0, #181818)"]]],"recovery-key-shield-check":[1024,[[59713,"var(--fill-0, #008836)"]]],"search":[1024,[[59714,"#181818"]]],"shield-check":[1024,[[59715,"#181818"]]],"shield":[1024,[[59716,"var(--fill-0, #181818)"]]],"shopping-mode":[1024,[[59717,"#666666"]]],"sliders-horizontal":[1024,[[59718,"var(--fill-0, #181818)"]]],"sliders":[1024,[[59719,"#181818"]]],"sort-ascending":[1024,[[59720,"#666666"]]],"sort-descending":[1024,[[59721,"currentColor"]]],"square-out":[1024,[[59722,"#181818"]]],"squares-four":[1024,[[59723,"var(--fill-0, #181818)"]]],"star-filled":[1024,[[59724,"#E5C131"]]],"star":[1024,[[59725,"#666666"]]],"sun":[1024,[[59726,"#666666"]]],"telegram-logo":[1024,[[59727,"var(--fill-0, #181818)"]]],"toggle-off":[1725,[[59728,"#DADADA"]]],"toggle-on":[1725,[[59729,"#008836"]]],"trash":[1024,[[59730,"#666666"]]],"user-circle-minus":[1024,[[59731,"var(--fill-0, #FF242E)"]]],"user-sheet-docs":[1024,[[59732,"var(--fill-0, #181818)"]]],"user-sheet-logout":[1024,[[59733,"var(--fill-0, #FB2C36)"]]],"user-sheet-settings":[1024,[[59734,"var(--fill-0, #181818)"]]],"user-sheet-support":[1024,[[59735,"var(--fill-0, #181818)"]]],"user-sheet-sync":[1024,[[59736,"var(--fill-0, #181818)"]]],"user":[1024,[[59737,"var(--fill-0, #181818)"]]],"users-three":[1024,[[59738,"var(--fill-0, #181818)"]]],"view-list":[1024,[[59739,"#181818"]]],"warning-circle":[1024,[[59740,"#BB3431"]]],"wifi-slash":[1024,[[59741,"var(--fill-0, #181818)"]]],"x-logo":[1024,[[59742,"var(--fill-0, #181818)"]]]}}
{"m":{"f":"notesnook-icons","u":1024,"z":1020,"s":59648,"h":"ed87d51c541700d863c75e1c0ba9f42fa7e5f8cc5549f0049cb3931307de6789"},"i":{"archive":[1024,[[59648,"#666666"]]],"arrow-back":[1024,[[59649,"rgba(255,255,255,0.7)"]]],"arrow-counter-clockwise":[1024,[[59650,"var(--fill-0, #181818)"]]],"arrow-fat-up":[1024,[[59651,"var(--fill-0, #181818)"]]],"arrow-u-up-left":[1024,[[59652,"var(--fill-0, #181818)"]]],"arrows-clockwise":[1024,[[59653,"var(--fill-0, #181818)"]]],"bag-simple":[1024,[[59654,"var(--fill-0, #181818)"]]],"bell":[1024,[[59655,"#666666"]]],"book-open":[1024,[[59656,"#666666"]]],"bookmark":[1024,[[59657,"#666666"]]],"box-empty":[1024,[[59658,"#B0B0B1"]]],"bug-droid":[1024,[[59659,"var(--fill-0, #181818)"]]],"calendar":[1024,[[59660,"rgba(102,102,102,0.8)"]]],"chart-line-up":[1024,[[59661,"var(--fill-0, #181818)"]]],"chat":[1024,[[59662,"white"]]],"checkbox":[1024,[[59663,"#008836"],[59664,"white"]]],"checks":[1024,[[59665,"#181818"]]],"chevron-down":[1024,[[59666,"#181818"]]],"chevron-right":[1024,[[59667,"#181818"]]],"chevron-up":[1024,[[59668,"#181818"]]],"clock-counter-clockwise":[1024,[[59669,"var(--fill-0, #181818)"]]],"close":[1024,[[59670,"#666666"]]],"cloud-check":[1024,[[59671,"var(--fill-0, #181818)"]]],"cloud":[1024,[[59672,"white"]]],"dark-mode-outline":[1024,[[59673,"currentColor"]]],"device-mobile-camera":[1024,[[59674,"var(--fill-0, #181818)"]]],"discord-logo":[1024,[[59675,"var(--fill-0, #181818)"]]],"dots-three":[1024,[[59676,"#202020"]]],"download-simple":[1024,[[59677,"var(--fill-0, #181818)"]]],"drive-file-move":[1088,[[59678,"#666666"]]],"edit-pencil":[1024,[[59679,"#181818"]]],"envelope-simple":[1024,[[59680,"white"]]],"export":[1024,[[59681,"var(--fill-0, #181818)"]]],"eye-closed":[1024,[[59682,"white"]]],"eye-open":[1024,[[59683,"currentColor"]]],"eye-slash":[1024,[[59684,"var(--fill-0, #181818)"]]],"file-dashed":[1024,[[59685,"var(--fill-0, #181818)"]]],"file-text":[1024,[[59686,"var(--fill-0, #181818)"]]],"file":[1024,[[59687,"white"]]],"funnel":[1024,[[59688,"#666666"]]],"git-pull-request":[1024,[[59689,"var(--fill-0, #181818)"]]],"github-logo":[1024,[[59690,"var(--fill-0, #181818)"]]],"hard-drives":[1024,[[59691,"var(--fill-0, #181818)"]]],"home":[1024,[[59692,"#181818"]]],"image-outline":[1024,[[59693,"white"]]],"key":[1024,[[59694,"var(--fill-0, #181818)"]]],"link":[560,[[59695,"#666666"]]],"lock-simple":[1024,[[59696,"white"]]],"lock":[1024,[[59697,"#181818"]]],"mastodon-logo":[1024,[[59698,"var(--fill-0, #181818)"]]],"menu":[1024,[[59699,"#181818"]]],"network":[1024,[[59700,"var(--fill-0, #181818)"]]],"note":[1024,[[59701,"#181818"]]],"notification":[1024,[[59702,"var(--fill-0, #181818)"]]],"numpad":[1024,[[59703,"#181818"]]],"paperclip":[1024,[[59704,"var(--fill-0, #181818)"]]],"pause":[1024,[[59705,"var(--fill-0, #181818)"]]],"pencil-simple-line":[1024,[[59706,"var(--fill-0, #181818)"]]],"pin":[939,[[59707,"#666666"]]],"plus":[1024,[[59708,"#666666"]]],"recovery-key-cloud-arrow-down":[1024,[[59709,"var(--fill-0, #181818)"]]],"recovery-key-copy":[1024,[[59710,"var(--stroke-0, #008836)"]]],"recovery-key-file":[1024,[[59711,"var(--fill-0, #181818)"]]],"recovery-key-key":[1024,[[59712,"var(--fill-0, #181818)"]]],"recovery-key-qr-code":[1024,[[59713,"var(--fill-0, #181818)"]]],"recovery-key-shield-check":[1024,[[59714,"var(--fill-0, #008836)"]]],"search":[1024,[[59715,"#181818"]]],"shield-check":[1024,[[59716,"#181818"]]],"shield-plus":[1024,[[59717,"#181818"]]],"shield":[1024,[[59718,"var(--fill-0, #181818)"]]],"shopping-mode":[1024,[[59719,"#666666"]]],"sliders-horizontal":[1024,[[59720,"var(--fill-0, #181818)"]]],"sliders":[1024,[[59721,"#181818"]]],"sort-ascending":[1024,[[59722,"#666666"]]],"sort-descending":[1024,[[59723,"currentColor"]]],"square-out":[1024,[[59724,"#181818"]]],"squares-four":[1024,[[59725,"var(--fill-0, #181818)"]]],"star-filled":[1024,[[59726,"#E5C131"]]],"star":[1024,[[59727,"#666666"]]],"sun":[1024,[[59728,"#666666"]]],"telegram-logo":[1024,[[59729,"var(--fill-0, #181818)"]]],"toggle-off":[1725,[[59730,"#DADADA"]]],"toggle-on":[1725,[[59731,"#008836"]]],"trash":[1024,[[59732,"#666666"]]],"user-circle-minus":[1024,[[59733,"var(--fill-0, #FF242E)"]]],"user-sheet-docs":[1024,[[59734,"var(--fill-0, #181818)"]]],"user-sheet-logout":[1024,[[59735,"var(--fill-0, #FB2C36)"]]],"user-sheet-settings":[1024,[[59736,"var(--fill-0, #181818)"]]],"user-sheet-support":[1024,[[59737,"var(--fill-0, #181818)"]]],"user-sheet-sync":[1024,[[59738,"var(--fill-0, #181818)"]]],"user":[1024,[[59739,"var(--fill-0, #181818)"]]],"users-three":[1024,[[59740,"var(--fill-0, #181818)"]]],"view-list":[1024,[[59741,"#181818"]]],"warning-circle":[1024,[[59742,"#BB3431"]]],"wifi-slash":[1024,[[59743,"var(--fill-0, #181818)"]]],"x-logo":[1024,[[59744,"var(--fill-0, #181818)"]]]}}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 3C5 3.19778 4.94135 3.39112 4.83147 3.55557C4.72159 3.72002 4.56541 3.84819 4.38268 3.92388C4.19996 3.99957 3.99889 4.01937 3.80491 3.98079C3.61093 3.9422 3.43275 3.84696 3.29289 3.70711C3.15304 3.56725 3.0578 3.38907 3.01922 3.19509C2.98063 3.00111 3.00043 2.80004 3.07612 2.61732C3.15181 2.43459 3.27998 2.27841 3.44443 2.16853C3.60888 2.05865 3.80222 2 4 2C4.26522 2 4.51957 2.10536 4.70711 2.29289C4.89464 2.48043 5 2.73478 5 3ZM8 2C7.80222 2 7.60888 2.05865 7.44443 2.16853C7.27998 2.27841 7.15181 2.43459 7.07612 2.61732C7.00043 2.80004 6.98063 3.00111 7.01922 3.19509C7.0578 3.38907 7.15304 3.56725 7.29289 3.70711C7.43275 3.84696 7.61093 3.9422 7.80491 3.98079C7.99889 4.01937 8.19996 3.99957 8.38268 3.92388C8.56541 3.84819 8.72159 3.72002 8.83147 3.55557C8.94135 3.39112 9 3.19778 9 3C9 2.73478 8.89464 2.48043 8.70711 2.29289C8.51957 2.10536 8.26522 2 8 2ZM12 4C12.1978 4 12.3911 3.94135 12.5556 3.83147C12.72 3.72159 12.8482 3.56541 12.9239 3.38268C12.9996 3.19996 13.0194 2.99889 12.9808 2.80491C12.9422 2.61093 12.847 2.43275 12.7071 2.29289C12.5673 2.15304 12.3891 2.0578 12.1951 2.01922C12.0011 1.98063 11.8 2.00043 11.6173 2.07612C11.4346 2.15181 11.2784 2.27998 11.1685 2.44443C11.0587 2.60888 11 2.80222 11 3C11 3.26522 11.1054 3.51957 11.2929 3.70711C11.4804 3.89464 11.7348 4 12 4ZM4 5.5C3.80222 5.5 3.60888 5.55865 3.44443 5.66853C3.27998 5.77841 3.15181 5.93459 3.07612 6.11732C3.00043 6.30004 2.98063 6.50111 3.01922 6.69509C3.0578 6.88907 3.15304 7.06725 3.29289 7.20711C3.43275 7.34696 3.61093 7.4422 3.80491 7.48079C3.99889 7.51937 4.19996 7.49957 4.38268 7.42388C4.56541 7.34819 4.72159 7.22002 4.83147 7.05557C4.94135 6.89112 5 6.69778 5 6.5C5 6.23478 4.89464 5.98043 4.70711 5.79289C4.51957 5.60536 4.26522 5.5 4 5.5ZM8 5.5C7.80222 5.5 7.60888 5.55865 7.44443 5.66853C7.27998 5.77841 7.15181 5.93459 7.07612 6.11732C7.00043 6.30004 6.98063 6.50111 7.01922 6.69509C7.0578 6.88907 7.15304 7.06725 7.29289 7.20711C7.43275 7.34696 7.61093 7.4422 7.80491 7.48079C7.99889 7.51937 8.19996 7.49957 8.38268 7.42388C8.56541 7.34819 8.72159 7.22002 8.83147 7.05557C8.94135 6.89112 9 6.69778 9 6.5C9 6.23478 8.89464 5.98043 8.70711 5.79289C8.51957 5.60536 8.26522 5.5 8 5.5ZM12 5.5C11.8022 5.5 11.6089 5.55865 11.4444 5.66853C11.28 5.77841 11.1518 5.93459 11.0761 6.11732C11.0004 6.30004 10.9806 6.50111 11.0192 6.69509C11.0578 6.88907 11.153 7.06725 11.2929 7.20711C11.4327 7.34696 11.6109 7.4422 11.8049 7.48079C11.9989 7.51937 12.2 7.49957 12.3827 7.42388C12.5654 7.34819 12.7216 7.22002 12.8315 7.05557C12.9414 6.89112 13 6.69778 13 6.5C13 6.23478 12.8946 5.98043 12.7071 5.79289C12.5196 5.60536 12.2652 5.5 12 5.5ZM4 9C3.80222 9 3.60888 9.05865 3.44443 9.16853C3.27998 9.27841 3.15181 9.43459 3.07612 9.61732C3.00043 9.80004 2.98063 10.0011 3.01922 10.1951C3.0578 10.3891 3.15304 10.5673 3.29289 10.7071C3.43275 10.847 3.61093 10.9422 3.80491 10.9808C3.99889 11.0194 4.19996 10.9996 4.38268 10.9239C4.56541 10.8482 4.72159 10.72 4.83147 10.5556C4.94135 10.3911 5 10.1978 5 10C5 9.73478 4.89464 9.48043 4.70711 9.29289C4.51957 9.10536 4.26522 9 4 9ZM8 9C7.80222 9 7.60888 9.05865 7.44443 9.16853C7.27998 9.27841 7.15181 9.43459 7.07612 9.61732C7.00043 9.80004 6.98063 10.0011 7.01922 10.1951C7.0578 10.3891 7.15304 10.5673 7.29289 10.7071C7.43275 10.847 7.61093 10.9422 7.80491 10.9808C7.99889 11.0194 8.19996 10.9996 8.38268 10.9239C8.56541 10.8482 8.72159 10.72 8.83147 10.5556C8.94135 10.3911 9 10.1978 9 10C9 9.73478 8.89464 9.48043 8.70711 9.29289C8.51957 9.10536 8.26522 9 8 9ZM8 12.5C7.80222 12.5 7.60888 12.5587 7.44443 12.6685C7.27998 12.7784 7.15181 12.9346 7.07612 13.1173C7.00043 13.3 6.98063 13.5011 7.01922 13.6951C7.0578 13.8891 7.15304 14.0673 7.29289 14.2071C7.43275 14.347 7.61093 14.4422 7.80491 14.4808C7.99889 14.5194 8.19996 14.4996 8.38268 14.4239C8.56541 14.3482 8.72159 14.22 8.83147 14.0556C8.94135 13.8911 9 13.6978 9 13.5C9 13.2348 8.89464 12.9804 8.70711 12.7929C8.51957 12.6054 8.26522 12.5 8 12.5ZM12 9C11.8022 9 11.6089 9.05865 11.4444 9.16853C11.28 9.27841 11.1518 9.43459 11.0761 9.61732C11.0004 9.80004 10.9806 10.0011 11.0192 10.1951C11.0578 10.3891 11.153 10.5673 11.2929 10.7071C11.4327 10.847 11.6109 10.9422 11.8049 10.9808C11.9989 11.0194 12.2 10.9996 12.3827 10.9239C12.5654 10.8482 12.7216 10.72 12.8315 10.5556C12.9414 10.3911 13 10.1978 13 10C13 9.73478 12.8946 9.48043 12.7071 9.29289C12.5196 9.10536 12.2652 9 12 9Z" fill="#181818"/>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 8C5.5 7.86739 5.55268 7.74021 5.64645 7.64645C5.74021 7.55268 5.86739 7.5 6 7.5H7.5V6C7.5 5.86739 7.55268 5.74021 7.64645 5.64645C7.74021 5.55268 7.86739 5.5 8 5.5C8.13261 5.5 8.25979 5.55268 8.35355 5.64645C8.44732 5.74021 8.5 5.86739 8.5 6V7.5H10C10.1326 7.5 10.2598 7.55268 10.3536 7.64645C10.4473 7.74021 10.5 7.86739 10.5 8C10.5 8.13261 10.4473 8.25979 10.3536 8.35355C10.2598 8.44732 10.1326 8.5 10 8.5H8.5V10C8.5 10.1326 8.44732 10.2598 8.35355 10.3536C8.25979 10.4473 8.13261 10.5 8 10.5C7.86739 10.5 7.74021 10.4473 7.64645 10.3536C7.55268 10.2598 7.5 10.1326 7.5 10V8.5H6C5.86739 8.5 5.74021 8.44732 5.64645 8.35355C5.55268 8.25979 5.5 8.13261 5.5 8ZM14 3.5V7C14 10.295 12.405 12.2919 11.0669 13.3869C9.62563 14.5656 8.19188 14.9663 8.12938 14.9825C8.04344 15.0059 7.95281 15.0059 7.86688 14.9825C7.80438 14.9663 6.3725 14.5656 4.92938 13.3869C3.595 12.2919 2 10.295 2 7V3.5C2 3.23478 2.10536 2.98043 2.29289 2.79289C2.48043 2.60536 2.73478 2.5 3 2.5H13C13.2652 2.5 13.5196 2.60536 13.7071 2.79289C13.8946 2.98043 14 3.23478 14 3.5ZM13 3.5H3V7C3 9.33125 3.86375 11.2194 5.56687 12.6131C6.29203 13.2062 7.11553 13.6675 8 13.9762C8.89619 13.6621 9.72999 13.1924 10.4631 12.5887C12.1462 11.1975 13 9.31687 13 7V3.5Z" fill="#181818"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB