Files
notesnook/apps/mobile/src/views/Editor/Functions.js
ammarahm-ed cc027d87fd refactor
2022-01-07 23:42:58 +05:00

1050 lines
26 KiB
JavaScript

import React, {createRef} from 'react';
import {Platform} from 'react-native';
import {presentDialog} from '../../components/Dialog/functions';
import {useEditorStore, useMenuStore, useTagStore} from '../../provider/stores';
import {DDS} from '../../services/DeviceDetection';
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
presentSheet,
ToastEvent
} from '../../services/EventManager';
import Navigation from '../../services/Navigation';
import PremiumService from '../../services/PremiumService';
import {editing} from '../../utils';
import {COLORS_NOTE, COLOR_SCHEME} from '../../utils/Colors';
import {hexToRGBA} from '../../utils/ColorUtils';
import {db} from '../../utils/database';
import {
eClearEditor,
eOnLoadNote,
eOpenTagsDialog,
eShowGetPremium,
eShowMergeDialog
} from '../../utils/Events';
import filesystem from '../../utils/filesystem';
import {openLinkInBrowser} from '../../utils/functions';
import {MMKV} from '../../utils/mmkv';
import {tabBarRef} from '../../utils/Refs';
import {normalize} from '../../utils/SizeUtils';
import {sleep, timeConverter} from '../../utils/TimeUtils';
import {TableCellProperties} from './TableCellProperties';
import {TableRowProperties} from './TableRowProperties';
import tiny from './tiny/tiny';
import {
IMAGE_TOOLTIP_CONFIG,
TABLE_TOOLTIP_CONFIG
} from './tiny/toolbar/config';
export let EditorWebView = createRef();
export const editorTitleInput = createRef();
export const sourceUri =
Platform.OS === 'android' ? 'file:///android_asset/' : 'Web.bundle/site/';
let lastEditTime = 0;
let EDITOR_SETTINGS = null;
let webviewOK = true;
let noteEdited = false;
let note = null;
let id = null;
let content = {
data: '',
type: 'tiny'
};
let title = '';
let saveCounter = 0;
let timer = null;
let webviewInit = false;
let appColors = COLOR_SCHEME;
let closingSession = false;
let currentEditingTimer = null;
let webviewTimer = null;
let requestedReload = false;
let cTimeout = null;
let disableSaving = false;
let isSaving = false;
let waitForContent = false;
let prevNoteContent = null;
let sessionId = null;
let historySessionId = null;
export function startClosingSession() {
closingSession = true;
}
export function setWebviewInit(init) {
webviewInit = init;
}
export function getWebviewInit() {
return webviewInit;
}
export function setColors(colors) {
if (colors) {
appColors = colors;
}
let theme = {...appColors, factor: normalize(1)};
if (note && note.color && !DDS.isLargeTablet()) {
theme.shade = hexToRGBA(COLORS_NOTE[note.color?.toLowerCase()], 0.15);
}
tiny.call(EditorWebView, tiny.updateTheme(JSON.stringify(theme)));
}
export function isNotedEdited() {
return noteEdited;
}
async function waitForEvent(event, caller, onend) {
return new Promise((resolve, reject) => {
let resolved;
let event_callback = () => {
eUnSubscribeEvent(event, event_callback);
resolved = true;
clearTimeout(resolved);
onend && onend();
resolve();
};
eSubscribeEvent(event, event_callback);
caller && caller();
resolved = setTimeout(() => {
resolve(true);
}, 2000);
});
}
export async function clearTimer(clear) {
clearTimeout(timer);
timer = null;
if (waitForContent && noteEdited) {
await waitForEvent('content_event', () => {
console.log('requested content');
tiny.call(EditorWebView, request_content);
});
}
waitForContent = false;
if (clear) {
if (!noteEdited) return;
if (
(content?.data &&
typeof content.data === 'string' &&
content.data?.trim().length > 0) ||
(title && title?.trim().length > 0)
) {
console.log('saving note');
await saveNote(true);
}
}
if (currentEditingTimer) {
clearTimeout(currentEditingTimer);
currentEditingTimer = null;
}
}
export const CHECK_STATUS = `(function() {
let msg = JSON.stringify({
data: true,
type: 'running',
sessionId:sessionId
});
window.ReactNativeWebView.postMessage(msg)
})();`;
const request_content = `(function() {
if (window.ReactNativeWebView) {
if (!editor) return;
editor.getHTML().then(function(html) {
window.ReactNativeWebView.postMessage(
JSON.stringify({
type: 'tiny',
value:html,
caller:"request_content",
sessionId:sessionId
})
);
}).catch(console.log)
}
})();`;
export function getNote() {
return note;
}
export function setNoteOnly(n) {
note = n;
}
export function disableEditing() {
noteEdited = false;
disableSaving = true;
}
export const textInput = createRef();
export function post(type, value = null) {
let message = {
type,
value,
sessionId: sessionId
};
if (type === 'html') {
}
EditorWebView.current?.postMessage(JSON.stringify(message));
}
export const _onShouldStartLoadWithRequest = async request => {
if (request.url.includes('https')) {
if (Platform.OS === 'ios' && !request.isTopFrame) return;
openLinkInBrowser(request.url, appColors)
.catch(e => {})
.then(r => {});
return false;
} else {
return true;
}
};
export function checkNote() {
return note && note.id;
}
async function setNote(item) {
note = item;
saveCounter = 0;
title = note.title;
id = note.id;
noteEdited = false;
lastEditTime = item.dateEdited;
if (note.locked) {
content.data = note.content.data;
content.type = note.content.type;
} else {
let data = await db.content.raw(note.contentId);
data = await db.content.insertPlaceholders(data, 'placeholder.svg');
if (!data) {
content.data = '';
content.type = 'tiny';
} else {
content.data = data.data;
content.type = data.type;
}
}
}
function clearNote() {
note = null;
title = '';
noteEdited = false;
historySessionId = null;
prevNoteContent = content.data;
isSaving = false;
id = null;
content = {
data: '',
type: 'tiny'
};
}
function randId(prefix) {
return Math.random()
.toString(36)
.replace('0.', prefix || '');
}
function makeSessionId(item) {
sessionId = item?.id ? item.id + randId('_session_') : randId('session_');
}
let loading_queue;
let loading_note = false;
export const loadNote = async item => {
if (loading_note && id) {
loading_queue = item;
check_session_status();
return;
} else {
loading_queue = null;
}
loading_note = true;
editing.currentlyEditing = true;
editing.movedAway = false;
if (closingSession) {
eSendEvent('loadingNote', item);
await waitForEvent('session_ended');
await sleep(300);
}
closingSession = true;
if (editing.isFocused) {
tiny.call(EditorWebView, tiny.blur);
}
if (item && item.type === 'new') {
if (getNote()) {
eSendEvent('loadingNote', item);
await clearEditor(true, true, true);
await sleep(1000);
}
eSendEvent('loadingNote');
eSendEvent('updateTags');
closingSession = false;
disableSaving = false;
lasstEditTime = 0;
clearNote();
noteEdited = false;
loading_queue = null;
makeSessionId(item);
useEditorStore.getState().setSessionId(sessionId);
await checkStatus(false);
if (Platform.OS === 'android') {
EditorWebView.current?.requestFocus();
setTimeout(() => {
textInput.current?.focus();
EditorWebView.current?.requestFocus();
tiny.call(EditorWebView, tiny.focusEditor);
}, 200);
} else {
await sleep(200);
tiny.call(EditorWebView, tiny.focusEditor);
}
if (EDITOR_SETTINGS) {
tiny.call(
EditorWebView,
EDITOR_SETTINGS.directionality === 'rtl'
? `tinymce.activeEditor.execCommand('mceDirectionRTL');`
: `tinymce.activeEditor.execCommand('mceDirectionLTR');`
);
}
requestedReload = true;
updateSessionStatus();
tiny.call(EditorWebView, tiny.notLoading);
} else {
if (id === item.id && !item.forced) {
console.log('note is already opened in editor');
closingSession = false;
eSendEvent('loadingNote');
return;
}
eSendEvent('loadingNote', item);
if (getNote()) {
await clearEditor(true, true, true);
}
closingSession = false;
disableSaving = false;
noteEdited = false;
await setNote(item);
webviewInit = false;
editing.isFocused = false;
if (
loading_queue &&
(loading_queue?.id !== item?.id || loading_queue?.type === 'new')
) {
clearNote();
loading_note = false;
loadNote(loading_queue);
return;
}
makeSessionId(item);
useEditorStore.getState().setSessionId(sessionId);
requestedReload = true;
if (await checkStatus(false)) {
updateSessionStatus();
}
setTimeout(() => {
useEditorStore.getState().setCurrentlyEditingNote(item.id);
}, 1);
}
loading_note = false;
};
export const checkStatus = async noreset => {
return new Promise(resolve => {
webviewOK = false;
console.log('checking status of webview');
clearTimeout(webviewTimer);
webviewTimer = null;
const onWebviewOk = () => {
webviewOK = true;
webviewInit = true;
clearTimeout(webviewTimer);
webviewTimer = null;
console.log('webviewOk:', webviewOK);
resolve(true);
eUnSubscribeEvent('webviewOk', onWebviewOk);
};
eSubscribeEvent('webviewOk', onWebviewOk);
setTimeout(
() => {
EditorWebView.current?.injectJavaScript(CHECK_STATUS);
},
Platform.OS === 'ios' ? 300 : 1
);
webviewTimer = setTimeout(() => {
console.log('webviewOK:', webviewOK, 'Reset blocked:', noreset);
if (!webviewOK && !noreset) {
webviewInit = false;
console.log('full reset');
EditorWebView = createRef();
eSendEvent('webviewreset');
resolve(true);
}
}, 1000);
});
};
function updateSessionStatus() {
tiny.call(
EditorWebView,
`(function () {
sessionId = '${sessionId}';
let msg = JSON.stringify({
data: true,
type: 'status',
sessionId: sessionId
});
window.ReactNativeWebView.postMessage(msg);
})();`
);
}
function isContentInvalid(content) {
return (
!content ||
content === '' ||
content.trim() === '' ||
content === '<p></p>' ||
content === '<p><br></p>' ||
content === '<p>&nbsp;</p>' ||
content === `<p><br data-mce-bogus="1"></p>`
);
}
function check_session_status() {
tiny.call(
EditorWebView,
`(function () {
if (window.ReactNativeWebView) {
if (!editor) return;
editor.getHTML().then(function (value) {
let status =
!value ||
value === '' ||
value.trim() === '' ||
value === '<p></p>' ||
value === '<p><br></p>' ||
value === '<p>&nbsp;</p>' ||
value === '<p><br data-mce-bogus="1"></p>';
window.ReactNativeWebView.postMessage(
JSON.stringify({
type: 'content_not_loaded',
value: status,
sessionId: sessionId
})
);
}).catch(console.log)
}
})();`
);
}
export const _onMessage = async evt => {
if (!evt || !evt.nativeEvent || !evt.nativeEvent.data) return;
let message = evt.nativeEvent.data;
try {
message = JSON.parse(message);
} catch (e) {
return;
}
switch (message.type) {
case 'tinyerror':
ToastEvent.show({
heading: 'Error saving note',
message: message.value,
type: 'error'
});
break;
case 'tableconfig':
showTableOptionsTooltip();
break;
case 'tablecelloptions':
console.log(message.value);
eSendEvent('updatecell', message.value);
presentSheet({
component: <TableCellProperties data={message.value} />
});
break;
case 'tablerowoptions':
console.log('tablerowoptions', message.value);
eSendEvent('updaterow', message.value);
presentSheet({
component: <TableRowProperties data={message.value} />
});
break;
case 'selectionvalue':
eSendEvent('selectionvalue', message.value);
console.log(message.value);
break;
case 'history':
eSendEvent('historyEvent', message.value);
break;
case 'noteedited':
if (message.sessionId !== sessionId) return;
console.log('noteedited');
noteEdited = true;
break;
case 'tiny':
if (message.sessionId !== sessionId) return;
if (prevNoteContent && message.value === prevNoteContent) {
prevNoteContent = null;
noteEdited = false;
return;
}
console.log('tiny content recieved');
noteEdited = true;
lastEditTime = Date.now();
content = {
type: message.type,
data: message.value
};
onNoteChange();
if (waitForContent) {
eSendEvent('content_event');
}
break;
case 'title':
if (message.sessionId !== sessionId) return;
if (message.value !== title) {
noteEdited = true;
lastEditTime = Date.now();
title = message.value;
eSendEvent('editorScroll', {
title: message.value
});
onNoteChange();
}
break;
case 'scroll':
eSendEvent('editorScroll', message);
break;
case 'attachment_download':
filesystem.downloadAttachment(message.value, true);
break;
case 'noteLoaded':
tiny.call(EditorWebView, tiny.notLoading);
eSendEvent('loadingNote');
break;
case 'premium':
let user = await db.user.getUser();
if (user && !user.isEmailConfirmed) {
await sleep(500);
PremiumService.showVerifyEmailDialog();
} else {
eSendEvent(eShowGetPremium, {
context: 'editor',
title: 'Get Notesnook Pro',
desc: 'Enjoy Full Rich Text Editor with Markdown Support!'
});
}
break;
case 'status':
if (sessionId !== message.sessionId) {
if (!sessionId) makeSessionId({id});
updateSessionStatus();
return;
}
if (!requestedReload && getNote()) {
check_session_status();
return;
}
requestedReload = false;
setColors(COLOR_SCHEME);
eSendEvent('webviewOk');
loading_note = false;
webviewInit = true;
webviewOK = true;
setTimeout(() => {
if (PremiumService.get()) {
tiny.call(EditorWebView, tiny.setMarkdown, true);
} else {
tiny.call(EditorWebView, tiny.removeMarkdown, true);
}
}, 300);
await loadNoteInEditor();
setTimeout(() => {
check_session_status();
}, 1500);
break;
case 'content_not_loaded':
loading_note = false;
if (isContentInvalid(content.data)) return;
if (message.sessionId !== sessionId) {
requestedReload = true;
updateSessionStatus();
return;
}
if (!id) return;
console.log('content not loaded');
if (message.value) {
console.log('reloading');
await loadNoteInEditor();
setTimeout(() => {
check_session_status();
}, 1500);
}
break;
case 'running':
eSendEvent('webviewOk');
webviewOK = true;
break;
case 'editorSettings':
EDITOR_SETTINGS = message.value;
eSendEvent('editorSettingsEvent', message.value);
break;
case 'imagepreview':
eSendEvent('ImagePreview', message.value);
break;
case 'imageoptions':
if (editing.tooltip === 'imageoptions') {
eSendEvent('showTooltip');
break;
}
showImageOptionsTooltip();
break;
case 'focus':
editing.focusType = message.value;
break;
case 'selectionchange':
eSendEvent('onSelectionChange', message.value);
break;
case 'notetag':
if (message.value) {
let _tag = JSON.parse(message.value);
console.log(_tag.title);
await db.notes.note(note.id).untag(_tag.title);
useTagStore.getState().setTags();
Navigation.setRoutesToUpdate([
Navigation.routeNames.Notes,
Navigation.routeNames.NotesPage,
Navigation.routeNames.Tags
]);
}
break;
case 'newtag':
eSendEvent(eOpenTagsDialog, note);
break;
default:
break;
}
};
function showImageOptionsTooltip() {
editing.tooltip = 'imageoptions';
eSendEvent('showTooltip', IMAGE_TOOLTIP_CONFIG);
}
function showTableOptionsTooltip() {
editing.tooltip = 'tableconfig';
console.log('showTooltip');
eSendEvent('showTooltip', TABLE_TOOLTIP_CONFIG);
}
function onNoteChange() {
clearTimeout(timer);
timer = null;
noteEdited = true;
timer = setTimeout(() => {
if (noteEdited) {
saveNote();
}
}, 300);
}
export async function clearEditor(
clear = true,
reset = true,
immediate = false
) {
try {
console.log('closing session: ', closingSession);
closingSession = true;
tiny.call(EditorWebView, tiny.isLoading);
if (clear) {
waitForContent = true;
await clearTimer(true);
}
disableSaving = true;
db.fs.cancel(getNote()?.id);
clearNote();
if (cTimeout) {
clearTimeout(cTimeout);
cTimeout = null;
}
sessionId = null;
let func = async () => {
try {
console.log('reset editor', closingSession);
reset && EditorWebView.current?.reload();
await waitForEvent('resetcomplete');
editing.focusType = null;
eSendEvent('historyEvent', {
undo: 0,
redo: 0
});
saveCounter = 0;
useEditorStore.getState().setCurrentlyEditingNote(null);
} catch (e) {}
};
if (immediate) {
await func();
} else {
cTimeout = setTimeout(func, 500);
}
} catch (e) {}
disableSaving = false;
eSendEvent('session_ended');
closingSession = false;
}
async function setNoteInEditorAfterSaving(oldId, currentId) {
if (oldId !== currentId) {
id = currentId;
note = db.notes.note(id);
if (note) {
note = note.data;
} else {
await sleep(500);
note = db.notes.note(id);
if (note) {
note = note.data;
}
}
}
}
async function addToCollection(id) {
switch (editing.actionAfterFirstSave.type) {
case 'topic': {
await db.notes.move(
{
topic: editing.actionAfterFirstSave.id,
id: editing.actionAfterFirstSave.notebook
},
id
);
editing.actionAfterFirstSave = {
type: null
};
Navigation.setRoutesToUpdate([
Navigation.routeNames.Notebooks,
Navigation.routeNames.NotesPage,
Navigation.routeNames.Notebook
]);
break;
}
case 'tag': {
await db.notes.note(id).tag(editing.actionAfterFirstSave.id);
editing.actionAfterFirstSave = {
type: null
};
Navigation.setRoutesToUpdate([
Navigation.routeNames.Tags,
Navigation.routeNames.NotesPage
]);
break;
}
case 'color': {
await db.notes.note(id).color(editing.actionAfterFirstSave.id);
editing.actionAfterFirstSave = {
type: null
};
Navigation.setRoutesToUpdate([Navigation.routeNames.NotesPage]);
useMenuStore.getState().setColorNotes();
break;
}
default: {
break;
}
}
}
export async function saveNote(preventUpdate) {
if (disableSaving || !noteEdited || (isSaving && !id)) return;
if (preventUpdate) {
noteEdited = false;
}
isSaving = true;
try {
if (id && !db.notes.note(id)) {
clearNote();
useEditorStore.getState().setCurrentlyEditingNote(null);
return;
}
let locked = false;
if (id) {
let _note = db.notes.note(id).data;
if (_note.conflicted) {
presentResolveConflictDialog(_note);
return;
}
locked = _note.locked;
}
if (!historySessionId) {
historySessionId = note?.dateEdited || Date.now();
}
let noteData = {
title,
content: {
data: content.data,
type: content.type
},
id: id,
sessionId: historySessionId
};
console.log(
'Note Saved:::',
'historySessionId:',
historySessionId,
'preventUpdate',
preventUpdate
);
if (!locked) {
let noteId = await db.notes.add(noteData);
if (!id || saveCounter < 3) {
Navigation.setRoutesToUpdate([
Navigation.routeNames.Notes,
Navigation.routeNames.Favorites,
Navigation.routeNames.NotesPage,
Navigation.routeNames.Notebook
]);
}
if (!id) {
await addToCollection(noteId);
}
if (!id && !preventUpdate) {
if (!title || title === '') {
console.log('posting title now');
post('titleplaceholder', db.notes.note(noteId)?.data?.title || '');
}
useEditorStore.getState().setCurrentlyEditingNote(noteId);
await setNoteInEditorAfterSaving(id, noteId);
saveCounter++;
}
} else {
noteData.contentId = note.contentId;
await db.vault.save(noteData);
}
if (!preventUpdate) {
Navigation.setRoutesToUpdate([
Navigation.routeNames.NotesPage,
Navigation.routeNames.Favorites,
Navigation.routeNames.Notes
]);
let n = db.notes.note(id)?.data?.dateEdited;
lastEditTime = n + 10;
tiny.call(
EditorWebView,
tiny.updateDateEdited(n ? timeConverter(n) : '')
);
tiny.call(EditorWebView, tiny.updateSavingState(!n ? '' : 'Saved'));
}
} catch (e) {
if (e.message === 'ERR_VAULT_LOCKED') {
console.log(e);
presentDialog({
input: true,
inputPlaceholder: 'Enter vault password',
title: 'Unlock note',
paragraph: 'To save note, unlock it',
positiveText: 'Unlock',
positivePress: async password => {
if (password && password.trim()) {
try {
await db.vault.unlock(password);
await saveNote();
} catch (e) {
console.log(e);
return false;
}
} else {
return false;
}
},
negativeText: 'Cancel',
onClose: () => {
noteEdited = false;
eSendEvent(eClearEditor);
}
});
}
console.log(e);
ToastEvent.show({
heading: 'Error saving note',
message: e.message,
type: 'error'
});
}
isSaving = false;
}
export async function onWebViewLoad(premium, colors) {
eSendEvent('resetcomplete');
setTimeout(() => {
if (premium) {
tiny.call(EditorWebView, tiny.setMarkdown, true);
} else {
tiny.call(EditorWebView, tiny.removeMarkdown, true);
}
}, 300);
eSendEvent('updateTags');
setColors(colors);
}
async function restoreEditorState() {
let appState = await MMKV.getItem('appState');
if (appState) {
appState = JSON.parse(appState);
if (
appState.editing &&
appState.note &&
!appState.note.locked &&
appState.note.id &&
Date.now() < appState.timestamp + 3600000
) {
editing.isRestoringState = true;
eSendEvent('loadingNote', appState.note);
editing.currentlyEditing = true;
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
setTimeout(() => {
eSendEvent(eOnLoadNote, appState.note);
}, 100);
MMKV.removeItem('appState');
editing.movedAway = false;
eSendEvent('load_overlay', 'hide_editor');
editing.isRestoringState = false;
return;
}
editing.isRestoringState = false;
return;
}
editing.isRestoringState = false;
}
export const presentResolveConflictDialog = _note => {
presentDialog({
title: 'Changes not saved',
paragraph: 'Please resolve conflicts to save changes',
positiveText: 'Resolve',
positivePress: () => {
eSendEvent(eShowMergeDialog, _note);
}
});
};
const loadNoteInEditor = async (keepHistory = true) => {
if (!webviewInit) return;
try {
if (note?.id) {
eSendEvent('updateTags');
post('title', title);
intent = false;
if (!content || !content.data || content?.data?.length === 0) {
tiny.call(
EditorWebView,
`
sessionId = "${sessionId}"
globalThis.isClearingNoteData = false;
window.ReactNativeWebView.postMessage(
JSON.stringify({
type: 'noteLoaded',
value: true,
sessionId:sessionId
}),
);
`
);
} else {
post('html', content.data);
}
if (id) {
db.attachments.downloadImages(id);
}
setColors();
tiny.call(
EditorWebView,
tiny.updateDateEdited(timeConverter(note.dateEdited))
);
tiny.call(EditorWebView, tiny.updateSavingState('Saved'));
} else {
await restoreEditorState();
}
loadingNote = null;
if (keepHistory) {
tiny.call(EditorWebView, tiny.clearHistory);
}
} catch (e) {}
disableSaving = false;
};
export async function updateNoteInEditor() {
return;
let _note = db.notes.note(id).data;
if (_note.conflicted) {
presentResolveConflictDialog(_note);
return;
}
let data = await db.content.raw(_note.contentId);
if (lastEditTime > _note.dateEdited) return;
if (data.data === content.data) return;
if (content.data.indexOf(data.data) !== -1) return;
if (note.dateEdited === _note.dateEdited) return;
title = note.title;
content.data = data.data;
note = _note;
lastEditTime = _note.dateEdited + 10;
tiny.call(EditorWebView, tiny.isLoading);
await setNote(_note);
tiny.call(EditorWebView, tiny.isLoading);
post('title', title);
post('inject', content.data);
setTimeout(() => {
tiny.call(EditorWebView, tiny.notLoading);
}, 50);
if (id) {
db.attachments.downloadImages(id);
}
tiny.call(EditorWebView, tiny.notLoading);
}