import React, {useEffect, useState} from 'react'; import {Platform} from 'react-native'; import { Clipboard, Dimensions, Keyboard, ScrollView, TouchableOpacity, View } from 'react-native'; import {FlatList} from 'react-native-gesture-handler'; import Share from 'react-native-share'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import {notesnook} from '../../../e2e/test.ids'; import {useTracked} from '../../provider'; import {Actions} from '../../provider/Actions'; import { useMenuStore, useSelectionStore, useTrashStore, useUserStore } from '../../provider/stores'; import {DDS} from '../../services/DeviceDetection'; import { eSendEvent, eSubscribeEvent, eUnSubscribeEvent, openVault, ToastEvent } from '../../services/EventManager'; import Navigation from '../../services/Navigation'; import Notifications from '../../services/Notifications'; import Sync from '../../services/Sync'; import {editing, toTXT} from '../../utils'; import { ACCENT, COLOR_SCHEME, COLOR_SCHEME_DARK, COLOR_SCHEME_LIGHT, setColorScheme } from '../../utils/Colors'; import {db} from '../../utils/DB'; import { eOpenMoveNoteDialog, eOpenPublishNoteDialog, eOpenTagsDialog } from '../../utils/Events'; import {deleteItems} from '../../utils/functions'; import {MMKV} from '../../utils/mmkv'; import {SIZE} from '../../utils/SizeUtils'; import {sleep, timeConverter} from '../../utils/TimeUtils'; import {Button} from '../Button'; import {presentDialog} from '../Dialog/functions'; import {PressableButton} from '../PressableButton'; import Heading from '../Typography/Heading'; import Paragraph from '../Typography/Paragraph'; import {ActionSheetColorsSection} from './ActionSheetColorsSection'; import {ActionSheetTagsSection} from './ActionSheetTagsSection'; import htmlToText from 'html-to-text'; const w = Dimensions.get('window').width; export const ActionSheetComponent = ({ close = () => {}, item, hasColors = false, hasTags = false, rowItems = [], getRef }) => { const [state, dispatch] = useTracked(); const {colors} = state; const clearSelection = useSelectionStore(state => state.clearSelection); const setSelectedItem = useSelectionStore(state => state.setSelectedItem); const setMenuPins = useMenuStore(state => state.setMenuPins); const [isPinnedToMenu, setIsPinnedToMenu] = useState( db.settings.isPinned(item.id) ); const [note, setNote] = useState(item); const user = useUserStore(state => state.user); const lastSynced = useUserStore(state => state.lastSynced); const [notifPinned, setNotifPinned] = useState(null); const refreshing = false; const isPublished = db.monographs.isPublished(note.id); const noteInTopic = editing.actionAfterFirstSave.type === 'topic' && db.notebooks .notebook(editing.actionAfterFirstSave.notebook) .topics.topic(editing.actionAfterFirstSave.id) .has(item.id); useEffect(() => { if (item.id === null) return; checkNotifPinned(); setNote({...item}); if (item.type !== 'note') { setIsPinnedToMenu(db.settings.isPinned(note.id)); } }, [item]); function checkNotifPinned() { let pinned = Notifications.getPinnedNotes(); console.log(pinned); if (!pinned) { setNotifPinned(null); return; } let index = pinned.findIndex(notif => notif.tag === item.id); if (index !== -1) { setNotifPinned(pinned[index]); } else { setNotifPinned(null); } } const onUpdate = async type => { console.log('update', type); if (type === 'unpin') { await sleep(1000); await Notifications.get(); checkNotifPinned(); } }; useEffect(() => { eSubscribeEvent('onUpdate', onUpdate); return () => { eUnSubscribeEvent('onUpdate', onUpdate); }; }, [item]); function changeColorScheme(colors = COLOR_SCHEME, accent = ACCENT) { let newColors = setColorScheme(colors, accent); dispatch({type: Actions.THEME, colors: newColors}); } const localRefresh = (type, nodispatch = false) => { if (!note || !note.id) return; let _item; switch (type) { case 'note': { _item = db.notes.note(note.id)?.data; break; } case 'notebook': { _item = db.notebooks.notebook(note.id)?.data; break; } case 'topic': { _item = db.notebooks.notebook(note.notebookId).topics.topic(note.title); break; } } if (!_item || !_item.id) return; if (!nodispatch) { Navigation.setRoutesToUpdate([ Navigation.routeNames.NotesPage, Navigation.routeNames.Favorites, Navigation.routeNames.Notes, Navigation.routeNames.Notebooks, Navigation.routeNames.Notebook, Navigation.routeNames.Tags, Navigation.routeNames.Trash ]); } setNote({..._item}); }; const rowItemsData = [ { name: 'Dark Mode', title: 'Dark mode', icon: 'theme-light-dark', func: () => { if (!colors.night) { MMKV.setStringAsync('theme', JSON.stringify({night: true})); changeColorScheme(COLOR_SCHEME_DARK); } else { MMKV.setStringAsync('theme', JSON.stringify({night: false})); changeColorScheme(COLOR_SCHEME_LIGHT); } }, switch: true, on: colors.night ? true : false, close: false, nopremium: true, id: notesnook.ids.dialogs.actionsheet.night }, { name: 'Add to notebook', title: 'Add to notebook', icon: 'book-outline', func: () => { close(); clearSelection(); setSelectedItem(note); setTimeout(() => { eSendEvent(eOpenMoveNoteDialog, note); }, 300); } }, { name: 'Pin', title: note.pinned ? 'Unpin' : 'Pin to top', icon: note.pinned ? 'pin-off-outline' : 'pin-outline', func: async () => { if (!note.id) return; close(); if (note.type === 'note') { if (db.notes.pinned.length === 3 && !note.pinned) { ToastEvent.show({ heading: 'Cannot pin more than 3 notes', type: 'error', context: 'local' }); return; } await db.notes.note(note.id).pin(); } else { if (db.notebooks.pinned.length === 3 && !note.pinned) { ToastEvent.show({ heading: 'Cannot pin more than 3 notebooks', type: 'error', context: 'local' }); return; } await db.notebooks.notebook(note.id).pin(); } localRefresh(item.type); }, close: false, check: true, on: note.pinned, nopremium: true, id: notesnook.ids.dialogs.actionsheet.pin }, { name: 'Favorite', title: !note.favorite ? 'Favorite' : 'Unfavorite', icon: note.favorite ? 'star-off' : 'star-outline', func: async () => { if (!note.id) return; close(); if (note.type === 'note') { await db.notes.note(note.id).favorite(); } else { await db.notebooks.notebook(note.id).favorite(); } Navigation.setRoutesToUpdate([ Navigation.routeNames.NotesPage, Navigation.routeNames.Favorites, Navigation.routeNames.Notes ]); localRefresh(item.type, true); }, close: false, check: true, on: note.favorite, nopremium: true, id: notesnook.ids.dialogs.actionsheet.favorite, color: 'orange' }, { name: 'PinToNotif', title: notifPinned !== null ? 'Unpin from Notifications' : 'Pin to Notifications', icon: 'bell', on: notifPinned !== null, func: async () => { if (Platform.OS === 'ios') return; if (notifPinned !== null) { Notifications.remove(note.id, notifPinned.identifier); await sleep(1000); await Notifications.get(); checkNotifPinned(); return; } if (note.locked) return; let text = await db.notes.note(note.id).content(); text = htmlToText.convert(text); Notifications.present({ title: note.title, message: note.headline, subtitle: note.headline, bigText: text, ongoing: true, actions: ['UNPIN'], tag: note.id }); await sleep(1000); let notifs = await Notifications.get(); checkNotifPinned(); } }, { name: 'Edit Notebook', title: 'Edit notebook', icon: 'square-edit-outline', func: () => { close('notebook'); } }, { name: 'Edit Topic', title: 'Edit topic', icon: 'square-edit-outline', func: () => { close('topic'); } }, { name: 'Copy', title: 'Copy', icon: 'content-copy', func: async () => { if (note.locked) { openVault({ copyNote: true, novault: true, locked: true, item: note, title: 'Copy note', description: 'Unlock note to copy to clipboard.' }); } else { let text = await db.notes.note(note.id).content(); text = htmlToText.convert(text); text = `${note.title}\n \n ${text}`; Clipboard.setString(text); ToastEvent.show({ heading: 'Note copied to clipboard', type: 'success', context: 'local' }); } } }, { name: 'Restore', title: 'Restore ' + note.itemType, icon: 'delete-restore', func: async () => { close(); await db.trash.restore(note.id); Navigation.setRoutesToUpdate([ Navigation.routeNames.Tags, Navigation.routeNames.Notes, Navigation.routeNames.Notebooks, Navigation.routeNames.NotesPage, Navigation.routeNames.Favorites, Navigation.routeNames.Trash ]); type = note.type === 'trash' ? note.itemType : note.type; ToastEvent.show({ heading: type === 'note' ? 'Note restored from trash' : 'Notebook restored from trash', type: 'success' }); } }, { name: 'Publish', title: 'Publish', icon: 'cloud-upload-outline', func: async () => { if (!user) { ToastEvent.show({ heading: 'Login required', message: 'Login to publish note', context: 'local', func: () => { eSendEvent(eOpenLoginDialog); }, actionText: 'Login' }); return; } if (!user.isEmailConfirmed) { ToastEvent.show({ heading: 'Email not verified', message: 'Please verify your email first.', context: 'local' }); return; } if (note.locked) { ToastEvent.show({ heading: 'Locked notes cannot be published', type: 'error', context: 'local' }); return; } close(); await sleep(300); eSendEvent(eOpenPublishNoteDialog, note); } }, { name: 'Vault', title: note.locked ? 'Remove from vault' : 'Add to vault', icon: note.locked ? 'shield-off-outline' : 'shield-outline', func: async () => { if (!note.id) return; if (note.locked) { close('unlock'); } else { db.vault .add(note.id) .then(r => { let n = db.notes.note(note.id).data; if (n.locked) { close(); } Navigation.setRoutesToUpdate([ Navigation.routeNames.NotesPage, Navigation.routeNames.Favorites, Navigation.routeNames.Notes ]); localRefresh(note.type); }) .catch(async e => { switch (e.message) { case db.vault.ERRORS.noVault: close('novault'); break; case db.vault.ERRORS.vaultLocked: close('locked'); break; case db.vault.ERRORS.wrongPassword: close(); break; } }); } }, on: note.locked }, { name: 'Add Shortcut', title: isPinnedToMenu ? 'Remove Shortcut' : 'Add Shortcut', icon: isPinnedToMenu ? 'link-variant-remove' : 'link-variant', func: async () => { close(); try { if (isPinnedToMenu) { await db.settings.unpin(note.id); } else { if (item.type === 'topic') { await db.settings.pin(note.type, { id: note.id, notebookId: note.notebookId }); } else { await db.settings.pin(note.type, {id: note.id}); } } setIsPinnedToMenu(db.settings.isPinned(note.id)); setMenuPins(); } catch (e) {} }, close: false, check: true, on: isPinnedToMenu, nopremium: true, id: notesnook.ids.dialogs.actionsheet.pinMenu }, { name: 'Edit Tag', title: 'Edit tag', icon: 'square-edit-outline', func: async () => { close(); await sleep(300); presentDialog({ title: 'Edit tag', paragraph: 'Change the title of the tag', positivePress: async value => { await db.tags.rename(note.id, value); Navigation.setRoutesToUpdate([ Navigation.routeNames.Notes, Navigation.routeNames.NotesPage, Navigation.routeNames.Tags ]); }, input: true, defaultValue: note.title, inputPlaceholder: 'Enter tag title', positiveText: 'Save' }); } }, { name: 'Share', title: 'Share', icon: 'share-variant', func: async () => { if (note.locked) { close(); openVault({ item: item, novault: true, locked: true, share: true, title: 'Share note', description: 'Unlock note to share it.' }); } else { let text = await db.notes.note(note.id).export('txt'); let m = `${note.title}\n \n ${text}`; Share.open({ title: 'Share note to', failOnCancel: false, message: m }); } } }, { name: 'Export', title: 'Export', icon: 'export', func: () => { close('export'); } }, { name: 'RemoveTopic', title: 'Remove from topic', hidden: !noteInTopic, icon: 'minus-circle-outline', func: async () => { await db.notebooks .notebook(editing.actionAfterFirstSave.notebook) .topics.topic(editing.actionAfterFirstSave.id) .delete(note.id); Navigation.setRoutesToUpdate([ Navigation.routeNames.Notebooks, Navigation.routeNames.Notes, Navigation.routeNames.NotesPage, Navigation.routeNames.Notebook ]); setNote(db.notes.note(note.id).data); close(); } }, { name: 'Delete', title: note.type !== 'notebook' && note.type !== 'note' ? 'Delete ' + item.type : 'Move to trash', icon: 'delete-outline', type: 'error', func: async () => { close(); if (note.type === 'tag') { await sleep(300); presentDialog({ title: 'Delete tag', paragraph: 'This tag will be removed from all notes.', positivePress: async value => { await db.tags.remove(note.id); Navigation.setRoutesToUpdate([ Navigation.routeNames.Notes, Navigation.routeNames.NotesPage, Navigation.routeNames.Tags ]); }, positiveText: 'Delete', positiveType: 'errorShade' }); return; } if (note.locked) { await sleep(300); openVault({ deleteNote: true, novault: true, locked: true, item: note, title: 'Delete note', description: 'Unlock note to delete it.' }); } else { try { close(); await deleteItems(note); } catch (e) {} } } }, { name: 'PermDelete', title: 'Delete ' + note.itemType, icon: 'delete', func: async () => { close(); await sleep(300); presentDialog({ title: `Permanent delete`, paragraph: `Are you sure you want to delete this ${note.itemType} permanantly from trash?`, positiveText: 'Delete', negativeText: 'Cancel', positivePress: async () => { await db.trash.delete(note.id); useTrashStore.getState().setTrash(); useSelectionStore.getState().setSelectionMode(false); ToastEvent.show({ heading: 'Permanantly deleted items', type: 'success', context: 'local' }); }, positiveType: 'errorShade' }); } } ]; let columnItemWidth = DDS.isTab ? (400 - 24) / rowItems.length : (w - 24) / 5; const _renderRowItem = rowItem => ( {rowItem.title} ); const onScrollEnd = () => { getRef().current?.handleChildScrollEnd(); }; return ( { if (!item.dateDeleted) { localRefresh(item.type, true); } }} style={{ backgroundColor: colors.bg, paddingHorizontal: 0, borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1, borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1 }}> { Keyboard.dismiss(); }} /> {!note || !note.id ? ( Start writing to save your note. ) : ( {note.type === 'tag' ? '#' : null} {note?.title.replace('\n', '')} {note.headline || note.description ? ( {note.type === 'notebook' && note.description ? note.description : null} {note.type === 'note' && note.headline ? note.headline[item.headline.length - 1] === '\n' ? note.headline.slice(0, note.headline.length - 1) : note.headline : null} ) : null} {note.type === 'note' || (note.type === 'tag' && note.dateEdited) ? 'Last edited on ' + timeConverter(note.dateEdited) : null} {note.type !== 'note' && note.type !== 'tag' && note.dateCreated && !note.dateDeleted ? ' Created on ' + timeConverter(note.dateCreated) : null} {note.dateDeleted ? 'Deleted on ' + timeConverter(note.dateDeleted) : null} {hasTags && note ? ( ) : null} {note.type === 'notebook' && note && note.topics && note.topics.length > 0 ? note.topics .sort((a, b) => a.dateEdited - b.dateEdited) .slice(0, 6) .map(topic => (