mobile: show merge conflict on locked notes

This commit is contained in:
Ammar Ahmed
2026-01-14 12:04:24 +05:00
committed by 01zulfi
parent 1544b656e0
commit b2f1fda641
8 changed files with 1190 additions and 965 deletions

View File

@@ -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>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
});
}}
/>

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { 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 = {

View File

@@ -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();
});

View File

@@ -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"

View File

@@ -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 ""

View File

@@ -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`
};