mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
mobile: show merge conflict on locked notes
This commit is contained in:
@@ -1,900 +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 Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { Component, createRef } from "react";
|
||||
import { InteractionManager, View } from "react-native";
|
||||
import Share from "react-native-share";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import BiometricService from "../../../services/biometrics";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import {
|
||||
eCloseActionSheet,
|
||||
eCloseVaultDialog,
|
||||
eOnLoadNote,
|
||||
eOpenVaultDialog,
|
||||
eUpdateNoteInEditor
|
||||
} from "../../../utils/events";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { fluidTabsRef } from "../../../utils/global-refs";
|
||||
import { convertNoteToText } from "../../../utils/note-to-text";
|
||||
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 Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
export class VaultDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visible: false,
|
||||
wrongPassword: false,
|
||||
loading: false,
|
||||
note: {},
|
||||
vault: false,
|
||||
locked: true,
|
||||
permanant: false,
|
||||
goToEditor: false,
|
||||
share: false,
|
||||
passwordsDontMatch: false,
|
||||
deleteNote: false,
|
||||
focusIndex: null,
|
||||
biometricUnlock: false,
|
||||
isBiometryEnrolled: false,
|
||||
isBiometryAvailable: false,
|
||||
fingerprintAccess: false,
|
||||
changePassword: false,
|
||||
copyNote: false,
|
||||
revokeFingerprintAccess: false,
|
||||
title: strings.goToEditor(),
|
||||
description: null,
|
||||
clearVault: false,
|
||||
deleteVault: false,
|
||||
deleteAll: false,
|
||||
noteLocked: false
|
||||
};
|
||||
|
||||
this.passInputRef = createRef();
|
||||
this.confirmPassRef = createRef();
|
||||
this.changePassInputRef = createRef();
|
||||
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
this.newPassword = null;
|
||||
this.title = !this.state.novault
|
||||
? strings.createVault()
|
||||
: this.state.fingerprintAccess
|
||||
? strings.vaultFingerprintUnlock()
|
||||
: this.state.revokeFingerprintAccess
|
||||
? strings.revokeVaultFingerprintUnlock()
|
||||
: this.state.changePassword
|
||||
? strings.changeVaultPassword()
|
||||
: this.state.noteLocked
|
||||
? this.state.deleteNote
|
||||
? strings.deleteNote()
|
||||
: this.state.share
|
||||
? strings.shareNote()
|
||||
: this.state.copyNote
|
||||
? strings.copyNote()
|
||||
: this.state.goToEditor
|
||||
? strings.goToEditor()
|
||||
: strings.goToEditor()
|
||||
: strings.lockNote();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
eSubscribeEvent(eOpenVaultDialog, this.open);
|
||||
eSubscribeEvent(eCloseVaultDialog, this.close);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
eUnSubscribeEvent(eOpenVaultDialog, this.open);
|
||||
eUnSubscribeEvent(eCloseVaultDialog, this.close);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('../../../services/event-manager').vaultType} data
|
||||
*/
|
||||
open = async (data) => {
|
||||
let biometry = await BiometricService.isBiometryAvailable();
|
||||
let available = false;
|
||||
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
|
||||
|
||||
if (biometry) {
|
||||
available = true;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
note: data.item,
|
||||
novault: data.novault,
|
||||
locked: data.locked,
|
||||
permanant: data.permanant,
|
||||
goToEditor: data.goToEditor,
|
||||
share: data.share,
|
||||
deleteNote: data.deleteNote,
|
||||
copyNote: data.copyNote,
|
||||
isBiometryAvailable: available,
|
||||
biometricUnlock: fingerprint,
|
||||
isBiometryEnrolled: fingerprint,
|
||||
fingerprintAccess: data.fingerprintAccess,
|
||||
changePassword: data.changePassword,
|
||||
revokeFingerprintAccess: data.revokeFingerprintAccess,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
clearVault: data.clearVault,
|
||||
deleteVault: data.deleteVault,
|
||||
noteLocked: data.item && (await db.vaults.itemExists(data.item))
|
||||
});
|
||||
|
||||
if (
|
||||
fingerprint &&
|
||||
data.novault &&
|
||||
!data.fingerprintAccess &&
|
||||
!data.revokeFingerprintAccess &&
|
||||
!data.changePassword &&
|
||||
!data.clearVault &&
|
||||
!data.deleteVault
|
||||
) {
|
||||
await this._onPressFingerprintAuth(data.title, data.description);
|
||||
} else {
|
||||
this.setState({
|
||||
visible: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
close = () => {
|
||||
if (this.state.loading) {
|
||||
ToastManager.show({
|
||||
heading: this.state.title,
|
||||
message: strings.pleaseWait() + "...",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
this.setState({
|
||||
visible: false,
|
||||
note: {},
|
||||
locked: false,
|
||||
permanant: false,
|
||||
goToEditor: false,
|
||||
share: false,
|
||||
novault: false,
|
||||
deleteNote: false,
|
||||
passwordsDontMatch: false
|
||||
});
|
||||
};
|
||||
|
||||
onPress = async () => {
|
||||
if (this.state.revokeFingerprintAccess) {
|
||||
await this._revokeFingerprintAccess();
|
||||
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
if (this.state.loading) return;
|
||||
|
||||
if (!this.password) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordNotEntered(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.novault) {
|
||||
if (this.password !== this.confirmPassword) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordNotMatched(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
passwordsDontMatch: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._createVault();
|
||||
} else if (this.state.changePassword) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
db.vault
|
||||
.changePassword(this.password, this.newPassword)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
if (this.state.biometricUnlock) {
|
||||
this._enrollFingerprint(this.newPassword);
|
||||
}
|
||||
ToastManager.show({
|
||||
heading: strings.passwordUpdated(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
})
|
||||
.catch((e) => {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
if (e.message === db.vault.ERRORS.wrongPassword) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
ToastManager.error(e);
|
||||
}
|
||||
});
|
||||
} else if (this.state.locked) {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
wrongPassword: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.state.noteLocked) {
|
||||
await this._unlockNote();
|
||||
} else {
|
||||
db.vault
|
||||
.unlock(this.password)
|
||||
.then(async () => {
|
||||
this.setState({
|
||||
wrongPassword: false
|
||||
});
|
||||
await this._lockNote();
|
||||
})
|
||||
.catch((e) => {
|
||||
this._takeErrorAction(e);
|
||||
});
|
||||
}
|
||||
} else if (this.state.fingerprintAccess) {
|
||||
this._enrollFingerprint(this.password);
|
||||
} else if (this.state.clearVault) {
|
||||
await this.clearVault();
|
||||
} else if (this.state.deleteVault) {
|
||||
await this.deleteVault();
|
||||
}
|
||||
};
|
||||
|
||||
deleteVault = async () => {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
try {
|
||||
let verified = true;
|
||||
if (await db.user.getUser()) {
|
||||
verified = await db.user.verifyPassword(this.password);
|
||||
}
|
||||
if (verified) {
|
||||
let noteIds = [];
|
||||
if (this.state.deleteAll) {
|
||||
const vault = await db.vaults.default();
|
||||
const relations = await db.relations.from(vault, "note").get();
|
||||
noteIds = relations.map((item) => item.toId);
|
||||
}
|
||||
await db.vault.delete(this.state.deleteAll);
|
||||
|
||||
if (this.state.deleteAll) {
|
||||
noteIds.forEach((id) => {
|
||||
eSendEvent(
|
||||
eUpdateNoteInEditor,
|
||||
{
|
||||
id: id,
|
||||
deleted: true
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
eSendEvent("vaultUpdated");
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.close();
|
||||
}, 100);
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
clearVault = async () => {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
try {
|
||||
const vault = await db.vaults.default();
|
||||
const relations = await db.relations.from(vault, "note").get();
|
||||
const noteIds = relations.map((item) => item.toId);
|
||||
|
||||
await db.vault.clear(this.password);
|
||||
|
||||
noteIds.forEach((id) => {
|
||||
eSendEvent(
|
||||
eUpdateNoteInEditor,
|
||||
{
|
||||
id: id,
|
||||
deleted: true
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
this.close();
|
||||
eSendEvent("vaultUpdated");
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
};
|
||||
|
||||
async _lockNote() {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
await db.vault.add(this.state.note.id);
|
||||
|
||||
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||
|
||||
this.close();
|
||||
ToastManager.show({
|
||||
message: strings.noteLocked(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _unlockNote() {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.state.permanant) {
|
||||
this._permanantUnlock();
|
||||
} else {
|
||||
await this._openNote();
|
||||
}
|
||||
}
|
||||
|
||||
_openNote = async () => {
|
||||
try {
|
||||
let note = await db.vault.open(this.state.note.id, this.password);
|
||||
if (this.state.biometricUnlock && !this.state.isBiometryEnrolled) {
|
||||
await this._enrollFingerprint(this.password);
|
||||
}
|
||||
|
||||
if (this.state.goToEditor) {
|
||||
this._openInEditor(note);
|
||||
} else if (this.state.share) {
|
||||
await this._shareNote(note);
|
||||
} else if (this.state.deleteNote) {
|
||||
await this._deleteNote();
|
||||
} else if (this.state.copyNote) {
|
||||
await this._copyNote(note);
|
||||
}
|
||||
} catch (e) {
|
||||
this._takeErrorAction(e);
|
||||
}
|
||||
};
|
||||
async _deleteNote() {
|
||||
try {
|
||||
await db.vault.remove(this.state.note.id, this.password);
|
||||
await deleteItems("note", [this.state.note.id]);
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this._takeErrorAction(e);
|
||||
}
|
||||
}
|
||||
|
||||
async _enrollFingerprint(password) {
|
||||
this.setState(
|
||||
{
|
||||
loading: true
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
await db.vault.unlock(password);
|
||||
await BiometricService.storeCredentials(password);
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
heading: strings.biometricUnlockEnabled(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this.close();
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async _createVault() {
|
||||
await db.vault.create(this.password);
|
||||
|
||||
if (this.state.biometricUnlock) {
|
||||
await this._enrollFingerprint(this.password);
|
||||
}
|
||||
if (this.state.note?.id) {
|
||||
await db.vault.add(this.state.note.id);
|
||||
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
ToastManager.show({
|
||||
heading: strings.noteLocked(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: strings.vaultCreated(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
eSendEvent("vaultUpdated");
|
||||
}
|
||||
|
||||
_permanantUnlock() {
|
||||
db.vault
|
||||
.remove(this.state.note.id, this.password)
|
||||
.then(() => {
|
||||
ToastManager.show({
|
||||
heading: strings.noteUnlocked(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||
this.close();
|
||||
})
|
||||
.catch((e) => {
|
||||
this._takeErrorAction(e);
|
||||
});
|
||||
}
|
||||
|
||||
_openInEditor(note) {
|
||||
this.close();
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
if (!DDS.isTab) {
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _copyNote(note) {
|
||||
Clipboard.setString((await convertNoteToText(note, true)) || "");
|
||||
ToastManager.show({
|
||||
heading: strings.noteCopied(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
|
||||
async _shareNote(note) {
|
||||
this.close();
|
||||
try {
|
||||
await Share.open({
|
||||
heading: note.title,
|
||||
failOnCancel: false,
|
||||
message: (await convertNoteToText(note)) || ""
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
_takeErrorAction(e) {
|
||||
this.setState({
|
||||
wrongPassword: true,
|
||||
visible: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
_revokeFingerprintAccess = async () => {
|
||||
try {
|
||||
await BiometricService.resetCredentials();
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
heading: strings.biometricUnlockDisabled(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
heading: e.message,
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onPressFingerprintAuth = async (title, description) => {
|
||||
try {
|
||||
let credentials = await BiometricService.getCredentials(
|
||||
title || this.state.title,
|
||||
description || this.state.description
|
||||
);
|
||||
|
||||
if (credentials?.password) {
|
||||
this.password = credentials.password;
|
||||
this.onPress();
|
||||
} else {
|
||||
eSendEvent(eCloseActionSheet);
|
||||
await sleep(300);
|
||||
this.setState({
|
||||
visible: true
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { colors } = this.props;
|
||||
const {
|
||||
note,
|
||||
visible,
|
||||
novault,
|
||||
deleteNote,
|
||||
share,
|
||||
goToEditor,
|
||||
fingerprintAccess,
|
||||
changePassword,
|
||||
loading,
|
||||
deleteVault,
|
||||
clearVault
|
||||
} = this.state;
|
||||
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<BaseDialog
|
||||
onShow={async () => {
|
||||
await sleep(100);
|
||||
|
||||
this.passInputRef.current?.focus();
|
||||
}}
|
||||
statusBarTranslucent={false}
|
||||
onRequestClose={this.close}
|
||||
visible={true}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
width: DDS.isTab ? 350 : "85%",
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
}}
|
||||
>
|
||||
<DialogHeader title={this.state.title} icon="shield" padding={12} />
|
||||
<Seperator half />
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
{(novault ||
|
||||
changePassword ||
|
||||
this.state.clearVault ||
|
||||
this.state.deleteVault) &&
|
||||
!this.state.revokeFingerprintAccess ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={this.passInputRef}
|
||||
editable={!loading}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwd}
|
||||
onChangeText={(value) => {
|
||||
this.password = value;
|
||||
}}
|
||||
marginBottom={
|
||||
!this.state.biometricUnlock ||
|
||||
!this.state.isBiometryEnrolled ||
|
||||
!novault ||
|
||||
changePassword
|
||||
? 0
|
||||
: 10
|
||||
}
|
||||
onSubmit={() => {
|
||||
changePassword
|
||||
? this.confirmPassRef.current?.focus()
|
||||
: this.onPress;
|
||||
}}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={
|
||||
changePassword ? strings.next() : this.state.title
|
||||
}
|
||||
returnKeyType={changePassword ? "next" : "done"}
|
||||
secureTextEntry
|
||||
placeholder={
|
||||
changePassword
|
||||
? strings.currentPassword()
|
||||
: strings.password()
|
||||
}
|
||||
/>
|
||||
|
||||
{!this.state.biometricUnlock ||
|
||||
!this.state.isBiometryEnrolled ||
|
||||
!novault ||
|
||||
changePassword ? null : (
|
||||
<Button
|
||||
onPress={() =>
|
||||
this._onPressFingerprintAuth(strings.unlockNote(), "")
|
||||
}
|
||||
icon="fingerprint"
|
||||
width="100%"
|
||||
title={strings.unlockWithBiometrics()}
|
||||
type="transparent"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{this.state.deleteVault && (
|
||||
<Button
|
||||
onPress={() =>
|
||||
this.setState({
|
||||
deleteAll: !this.state.deleteAll
|
||||
})
|
||||
}
|
||||
icon={
|
||||
this.state.deleteAll
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
width="100%"
|
||||
title={strings.deleteAllNotes()}
|
||||
type="errorShade"
|
||||
/>
|
||||
)}
|
||||
|
||||
{changePassword ? (
|
||||
<>
|
||||
<Seperator half />
|
||||
<Input
|
||||
ref={this.confirmPassRef}
|
||||
editable={!loading}
|
||||
testID={notesnook.ids.dialogs.vault.changePwd}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
this.newPassword = value;
|
||||
}}
|
||||
autoComplete="password"
|
||||
onSubmit={this.onPress}
|
||||
returnKeyLabel="Change"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
placeholder={strings.newPassword()}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{!novault ? (
|
||||
<View>
|
||||
<Input
|
||||
fwdRef={this.passInputRef}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwd}
|
||||
onChangeText={(value) => {
|
||||
this.password = value;
|
||||
}}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
onSubmit={() => {
|
||||
this.confirmPassRef.current?.focus();
|
||||
}}
|
||||
placeholder={strings.password()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={this.confirmPassRef}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwdAlt}
|
||||
secureTextEntry
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => this.password}
|
||||
errorMessage="Passwords do not match."
|
||||
onErrorCheck={() => null}
|
||||
marginBottom={0}
|
||||
autoComplete="password"
|
||||
returnKeyLabel="Create"
|
||||
returnKeyType="done"
|
||||
onChangeText={(value) => {
|
||||
this.confirmPassword = value;
|
||||
if (value !== this.password) {
|
||||
this.setState({
|
||||
passwordsDontMatch: true
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
passwordsDontMatch: false
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSubmit={this.onPress}
|
||||
placeholder={strings.confirmPassword()}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{this.state.biometricUnlock &&
|
||||
!this.state.isBiometryEnrolled &&
|
||||
novault ? (
|
||||
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
|
||||
) : null}
|
||||
|
||||
{this.state.isBiometryAvailable &&
|
||||
!this.state.fingerprintAccess &&
|
||||
!this.state.clearVault &&
|
||||
!this.state.deleteVault &&
|
||||
((!this.state.biometricUnlock && !changePassword) || !novault) ? (
|
||||
<Button
|
||||
onPress={() => {
|
||||
this.setState({
|
||||
biometricUnlock: !this.state.biometricUnlock
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
icon="fingerprint"
|
||||
width="100%"
|
||||
title={strings.unlockWithBiometrics()}
|
||||
iconColor={
|
||||
this.state.biometricUnlock
|
||||
? colors.selected.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
type={this.state.biometricUnlock ? "transparent" : "plain"}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<DialogButtons
|
||||
onPressNegative={this.close}
|
||||
onPressPositive={this.onPress}
|
||||
loading={loading}
|
||||
positiveType={
|
||||
deleteVault || clearVault ? "errorShade" : "transparent"
|
||||
}
|
||||
positiveTitle={
|
||||
deleteVault
|
||||
? strings.delete()
|
||||
: clearVault
|
||||
? strings.clear()
|
||||
: fingerprintAccess
|
||||
? strings.enable()
|
||||
: this.state.revokeFingerprintAccess
|
||||
? strings.revoke()
|
||||
: changePassword
|
||||
? strings.change()
|
||||
: this.state.noteLocked
|
||||
? deleteNote
|
||||
? strings.delete()
|
||||
: share
|
||||
? strings.share()
|
||||
: goToEditor
|
||||
? strings.open()
|
||||
: strings.unlock()
|
||||
: !note.id
|
||||
? strings.create()
|
||||
: strings.lock()
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
1001
apps/mobile/app/components/dialogs/vault/index.tsx
Normal file
1001
apps/mobile/app/components/dialogs/vault/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import {
|
||||
EncryptedContentItem,
|
||||
isEncryptedContent,
|
||||
Note,
|
||||
UnencryptedContentItem
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import KeepAwake from "@sayem314/react-native-keep-awake";
|
||||
import { diff } from "diffblazer";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { SafeAreaView, Text, View } from "react-native";
|
||||
import Animated from "react-native-reanimated";
|
||||
@@ -32,13 +40,16 @@ import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
eUnSubscribeEvent,
|
||||
openVault
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import Sync from "../../services/sync";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Dialog } from "../dialog";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import DialogButtons from "../dialog/dialog-buttons";
|
||||
import DialogContainer from "../dialog/dialog-container";
|
||||
@@ -47,46 +58,55 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { diff } from "diffblazer";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
|
||||
const MergeConflicts = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [keep, setKeep] = useState(null);
|
||||
const [copy, setCopy] = useState(null);
|
||||
const [selectedContent, setSelectedContent] =
|
||||
useState<UnencryptedContentItem>();
|
||||
const [copyOfDiscardedContent, setCopyOfDiscardedContent] =
|
||||
useState<UnencryptedContentItem>();
|
||||
const [dialogVisible, setDialogVisible] = useState(false);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const content = useRef(null);
|
||||
const isKeepingConflicted = !keep?.conflicted;
|
||||
const isKeeping = !!keep;
|
||||
const content = useRef<UnencryptedContentItem>(null);
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
|
||||
const applyChanges = async () => {
|
||||
let _content = keep;
|
||||
let note = await db.notes.note(_content.noteId);
|
||||
let contentToSave = selectedContent;
|
||||
if (!contentToSave) return;
|
||||
let note = await db.notes.note(contentToSave.noteId);
|
||||
if (!note) return;
|
||||
await db.notes.add({
|
||||
id: note.id,
|
||||
conflicted: false,
|
||||
dateEdited: _content.dateEdited
|
||||
dateEdited: contentToSave.dateEdited
|
||||
});
|
||||
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
data: _content.data,
|
||||
type: _content.type,
|
||||
dateResolved: content.current?.conflicted?.dateModified || Date.now(),
|
||||
sessionId: Date.now(),
|
||||
conflicted: false
|
||||
});
|
||||
const noteLocked = await db.vaults.itemExists(note);
|
||||
|
||||
if (copy) {
|
||||
if (noteLocked) {
|
||||
await db.vault.save({
|
||||
...contentToSave,
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
} else {
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
data: contentToSave.data,
|
||||
type: contentToSave.type,
|
||||
dateResolved: content.current?.conflicted?.dateModified || Date.now(),
|
||||
sessionId: `${Date.now()}`,
|
||||
conflicted: undefined
|
||||
});
|
||||
}
|
||||
|
||||
if (copyOfDiscardedContent) {
|
||||
await db.notes.add({
|
||||
title: note.title + " (Copy)",
|
||||
content: {
|
||||
data: copy.data,
|
||||
type: copy.type
|
||||
data: copyOfDiscardedContent.data,
|
||||
type: copyOfDiscardedContent.type
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -103,15 +123,86 @@ const MergeConflicts = () => {
|
||||
Sync.run();
|
||||
};
|
||||
|
||||
const show = async (item) => {
|
||||
let noteContent = await db.content.get(item.contentId);
|
||||
content.current = { ...noteContent };
|
||||
if (__DEV__) {
|
||||
if (!noteContent.conflicted) {
|
||||
content.current.conflicted = { ...noteContent };
|
||||
const show = async (item: Note) => {
|
||||
const isLocked = await db.vaults.itemExists(item);
|
||||
let noteContent: UnencryptedContentItem;
|
||||
if (isLocked) {
|
||||
openVault({
|
||||
item: item,
|
||||
novault: true,
|
||||
customActionTitle: "Unlock note",
|
||||
customActionParagraph: "Unlock note to merge conflicts",
|
||||
onUnlock: async (item, password) => {
|
||||
if (!item || !password) return;
|
||||
const currentContent = await db.content.get(item.contentId!);
|
||||
|
||||
try {
|
||||
noteContent = {
|
||||
...(await db.content.get(item.contentId!)),
|
||||
...item.content,
|
||||
conflicted: currentContent?.conflicted
|
||||
? await db.vault.decryptContent(
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
: undefined
|
||||
} as UnencryptedContentItem;
|
||||
|
||||
content.current = noteContent;
|
||||
if (__DEV__) {
|
||||
if (!noteContent?.conflicted) {
|
||||
content.current.conflicted = noteContent;
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
} catch (e) {
|
||||
presentDialog({
|
||||
input: true,
|
||||
inputPlaceholder: strings.enterPassword(),
|
||||
title: strings.unlockIncomingNote(),
|
||||
paragraph: strings.unlockIncomingNoteDesc(),
|
||||
positiveText: "Unlock",
|
||||
positivePress: async (password) => {
|
||||
try {
|
||||
noteContent = {
|
||||
...(await db.content.get(item.contentId!)),
|
||||
...item.content,
|
||||
conflicted: currentContent?.conflicted
|
||||
? await db.vault.decryptContent(
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
: undefined
|
||||
} as UnencryptedContentItem;
|
||||
|
||||
content.current = noteContent;
|
||||
if (__DEV__) {
|
||||
if (!noteContent?.conflicted) {
|
||||
content.current.conflicted = noteContent;
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
noteContent = (await db.content.get(
|
||||
item.contentId!
|
||||
)) as UnencryptedContentItem;
|
||||
content.current = noteContent;
|
||||
if (__DEV__) {
|
||||
if (!noteContent?.conflicted) {
|
||||
content.current.conflicted = noteContent;
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -123,8 +214,8 @@ const MergeConflicts = () => {
|
||||
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
setCopy(null);
|
||||
setKeep(null);
|
||||
setCopyOfDiscardedContent(undefined);
|
||||
setSelectedContent(undefined);
|
||||
setDialogVisible(false);
|
||||
};
|
||||
|
||||
@@ -134,6 +225,12 @@ const MergeConflicts = () => {
|
||||
back,
|
||||
isCurrent,
|
||||
contentToKeep
|
||||
}: {
|
||||
isDiscarded: boolean;
|
||||
keeping: boolean;
|
||||
back: boolean;
|
||||
isCurrent: boolean;
|
||||
contentToKeep: UnencryptedContentItem;
|
||||
}) => {
|
||||
return (
|
||||
<View
|
||||
@@ -194,7 +291,7 @@ const MergeConflicts = () => {
|
||||
{isDiscarded ? (
|
||||
<Button
|
||||
onPress={() => {
|
||||
setCopy(contentToKeep);
|
||||
setCopyOfDiscardedContent(contentToKeep);
|
||||
setDialogVisible(true);
|
||||
}}
|
||||
title={strings.saveACopy()}
|
||||
@@ -222,7 +319,6 @@ const MergeConflicts = () => {
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
color={colors.error.paragraph}
|
||||
onPress={() => {
|
||||
setDialogVisible(true);
|
||||
}}
|
||||
@@ -244,7 +340,9 @@ const MergeConflicts = () => {
|
||||
keeping && !isDiscarded ? strings.undo() : strings.keep()
|
||||
}
|
||||
onPress={() => {
|
||||
setKeep(keeping && !isDiscarded ? null : contentToKeep);
|
||||
setSelectedContent(
|
||||
keeping && !isDiscarded ? undefined : contentToKeep
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -258,7 +356,6 @@ const MergeConflicts = () => {
|
||||
<BaseDialog
|
||||
statusBarTranslucent
|
||||
transparent={false}
|
||||
animationType="slide"
|
||||
animated={false}
|
||||
bounce={false}
|
||||
onRequestClose={() => {
|
||||
@@ -266,15 +363,9 @@ const MergeConflicts = () => {
|
||||
}}
|
||||
centered={false}
|
||||
background={colors?.primary.background}
|
||||
supportedOrientations={[
|
||||
"portrait",
|
||||
"portrait-upside-down",
|
||||
"landscape",
|
||||
"landscape-left",
|
||||
"landscape-right"
|
||||
]}
|
||||
visible={true}
|
||||
>
|
||||
<Dialog context="merge-conflicts" />
|
||||
<SafeAreaView
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
@@ -301,15 +392,15 @@ const MergeConflicts = () => {
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: DDS.isLargeTablet() ? "rgba(0,0,0,0.3)" : null
|
||||
backgroundColor: DDS.isLargeTablet() ? "rgba(0,0,0,0.3)" : undefined
|
||||
}}
|
||||
>
|
||||
<ConfigBar
|
||||
back={true}
|
||||
isCurrent={true}
|
||||
isDiscarded={isKeeping && isKeepingConflicted}
|
||||
keeping={isKeeping}
|
||||
contentToKeep={content.current}
|
||||
isDiscarded={!!selectedContent && !selectedContent.conflicted}
|
||||
keeping={!!selectedContent}
|
||||
contentToKeep={content.current!}
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
@@ -323,15 +414,18 @@ const MergeConflicts = () => {
|
||||
<ReadonlyEditor
|
||||
editorId="conflictPrimary"
|
||||
onLoad={async (loadContent) => {
|
||||
const note = await db.notes.note(content.current?.noteId);
|
||||
const note = await db.notes.note(content.current!.noteId);
|
||||
if (!note) return;
|
||||
loadContent({
|
||||
id: note.id,
|
||||
data: diff(
|
||||
content.current.conflicted.data,
|
||||
content.current.data
|
||||
)
|
||||
});
|
||||
if (content.current && content.current.conflicted) {
|
||||
loadContent({
|
||||
id: note.id,
|
||||
data: diff(
|
||||
(content.current.conflicted as UnencryptedContentItem)
|
||||
.data,
|
||||
content.current.data
|
||||
)
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
@@ -339,9 +433,11 @@ const MergeConflicts = () => {
|
||||
<ConfigBar
|
||||
back={false}
|
||||
isCurrent={false}
|
||||
isDiscarded={isKeeping && !isKeepingConflicted}
|
||||
keeping={isKeeping}
|
||||
contentToKeep={content.current.conflicted}
|
||||
isDiscarded={!!selectedContent && !!selectedContent.conflicted}
|
||||
keeping={!!selectedContent}
|
||||
contentToKeep={
|
||||
content.current!.conflicted! as UnencryptedContentItem
|
||||
}
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
@@ -354,11 +450,12 @@ const MergeConflicts = () => {
|
||||
<ReadonlyEditor
|
||||
editorId="conflictSecondary"
|
||||
onLoad={async (loadContent) => {
|
||||
const note = await db.notes.note(content.current?.noteId);
|
||||
const note = await db.notes.note(content.current?.noteId!);
|
||||
if (!note) return;
|
||||
loadContent({
|
||||
id: note.id,
|
||||
data: content.current.conflicted.data
|
||||
data: (content.current!.conflicted as UnencryptedContentItem)
|
||||
.data
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { EventHandler, EventManager } from "@notesnook/core";
|
||||
import { EventHandler, EventManager, Note, NoteContent } from "@notesnook/core";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import { RefObject } from "react";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
@@ -32,8 +32,8 @@ import {
|
||||
} from "../utils/events";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type Vault = {
|
||||
item: unknown;
|
||||
export type Vault = {
|
||||
item: Note;
|
||||
novault: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -48,6 +48,14 @@ type Vault = {
|
||||
clearVault: boolean;
|
||||
deleteVault: boolean;
|
||||
copyNote: boolean;
|
||||
customActionTitle: string;
|
||||
customActionParagraph: string;
|
||||
onUnlock: (
|
||||
item: Note & {
|
||||
content?: NoteContent<false>;
|
||||
},
|
||||
password: string
|
||||
) => void;
|
||||
};
|
||||
|
||||
type NoteEdit = {
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("VAULT", () => {
|
||||
.typeTextById(notesnook.ids.dialogs.vault.pwd, "1234")
|
||||
.typeTextById(notesnook.ids.dialogs.vault.changePwd, "2362")
|
||||
.waitAndTapByText("Change")
|
||||
.pressBack(4)
|
||||
.pressBack(3)
|
||||
.addStep(async () => await openLockedNote("2362"))
|
||||
.run();
|
||||
});
|
||||
|
||||
@@ -6439,6 +6439,10 @@ msgstr "Thank you. You are the proof that privacy always comes first."
|
||||
msgid "The {title} at {url} is not compatible with this client."
|
||||
msgstr "The {title} at {url} is not compatible with this client."
|
||||
|
||||
#: src/strings.ts:2629
|
||||
msgid "The incoming note could not be unlocked with the provided password. Enter the correct password for the incoming note"
|
||||
msgstr "The incoming note could not be unlocked with the provided password. Enter the correct password for the incoming note"
|
||||
|
||||
#: src/strings.ts:231
|
||||
msgid "The information above will be publically available at"
|
||||
msgstr "The information above will be publically available at"
|
||||
@@ -6738,6 +6742,10 @@ msgstr "Unlink notebook"
|
||||
msgid "Unlock"
|
||||
msgstr "Unlock"
|
||||
|
||||
#: src/strings.ts:2627
|
||||
msgid "Unlock incoming note"
|
||||
msgstr "Unlock incoming note"
|
||||
|
||||
#: src/strings.ts:1383
|
||||
msgid "Unlock more colors with Notesnook Pro"
|
||||
msgstr "Unlock more colors with Notesnook Pro"
|
||||
|
||||
@@ -6398,6 +6398,10 @@ msgstr ""
|
||||
msgid "The {title} at {url} is not compatible with this client."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2629
|
||||
msgid "The incoming note could not be unlocked with the provided password. Enter the correct password for the incoming note"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:231
|
||||
msgid "The information above will be publically available at"
|
||||
msgstr ""
|
||||
@@ -6697,6 +6701,10 @@ msgstr ""
|
||||
msgid "Unlock"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2627
|
||||
msgid "Unlock incoming note"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1383
|
||||
msgid "Unlock more colors with Notesnook Pro"
|
||||
msgstr ""
|
||||
|
||||
@@ -2623,5 +2623,8 @@ Use this if changes from other devices are not appearing on this device. This wi
|
||||
weekFormat: () => t`Week format`,
|
||||
weekFormatDesc: () =>
|
||||
t`Choose what day to display as the first day of the week`,
|
||||
editCreationDate: () => t`Edit creation date`
|
||||
editCreationDate: () => t`Edit creation date`,
|
||||
unlockIncomingNote: () => t`Unlock incoming note`,
|
||||
unlockIncomingNoteDesc: () =>
|
||||
t`The incoming note could not be unlocked with the provided password. Enter the correct password for the incoming note`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user