From 22cd07e414f94f9b6733550a2f1388ee6dca5c57 Mon Sep 17 00:00:00 2001 From: ammarahm-ed Date: Thu, 23 Mar 2023 12:22:20 +0500 Subject: [PATCH] mobile: improve selection performance --- .../app/components/sheets/add-to/context.js | 4 - .../app/components/sheets/add-to/index.js | 131 ++++++++---------- .../app/components/sheets/add-to/list-item.js | 116 +++++++++------- .../app/components/sheets/add-to/store.ts | 42 ++++++ 4 files changed, 167 insertions(+), 126 deletions(-) create mode 100644 apps/mobile/app/components/sheets/add-to/store.ts diff --git a/apps/mobile/app/components/sheets/add-to/context.js b/apps/mobile/app/components/sheets/add-to/context.js index 4307e2f76..02335cf7c 100644 --- a/apps/mobile/app/components/sheets/add-to/context.js +++ b/apps/mobile/app/components/sheets/add-to/context.js @@ -20,13 +20,9 @@ along with this program. If not, see . import { createContext, useContext } from "react"; export const SelectionContext = createContext({ - enabled: false, - selected: [], toggleSelection: (item) => null, deselect: (item) => null, select: (item) => null, - isSelected: (item) => null, - setMultiSelect: () => null, deselectAll: () => null }); export const SelectionProvider = SelectionContext.Provider; diff --git a/apps/mobile/app/components/sheets/add-to/index.js b/apps/mobile/app/components/sheets/add-to/index.js index e8f08abb0..9aca6b8f9 100644 --- a/apps/mobile/app/components/sheets/add-to/index.js +++ b/apps/mobile/app/components/sheets/add-to/index.js @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo } from "react"; import { Keyboard, TouchableOpacity, View } from "react-native"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { db } from "../../../common/database"; @@ -35,10 +35,10 @@ import Paragraph from "../../ui/typography/paragraph"; import { SelectionProvider } from "./context"; import { FilteredList } from "./filtered-list"; import { ListItem } from "./list-item"; +import { useItemSelectionStore } from "./store"; const MoveNoteSheet = ({ note, actionSheetRef }) => { const colors = useThemeStore((state) => state.colors); - const [multiSelect, setMultiSelect] = useState(false); const notebooks = useNotebookStore((state) => state.notebooks.filter((n) => n?.type === "notebook") ); @@ -47,7 +47,9 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => { (state) => state.selectedItemsList ); const setNotebooks = useNotebookStore((state) => state.setNotebooks); - const [itemState, setItemState] = useState({}); + + const multiSelect = useItemSelectionStore((state) => state.multiSelect); + const onAddNotebook = async (title) => { if (!title || title.trim().length === 0) { ToastEvent.show({ @@ -135,40 +137,39 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => { const resetItemState = useCallback( (state) => { - setItemState(() => { - const itemState = {}; - const notebooks = db.notebooks.all; - for (let notebook of notebooks) { - itemState[notebook.id] = state + const itemState = {}; + const notebooks = db.notebooks.all; + for (let notebook of notebooks) { + itemState[notebook.id] = state + ? state + : areAllSelectedItemsInNotebook(notebook, selectedItemsList) + ? "selected" + : getSelectedNotesCountInItem(notebook, selectedItemsList) > 0 + ? "intermediate" + : "deselected"; + if (itemState[notebook.id] === "selected") { + contextValue.select(notebook); + } else { + contextValue.deselect(notebook); + } + for (let topic of notebook.topics) { + itemState[topic.id] = state ? state - : areAllSelectedItemsInNotebook(notebook, selectedItemsList) + : areAllSelectedItemsInTopic(topic, selectedItemsList) && + getSelectedNotesCountInItem(topic, selectedItemsList) ? "selected" - : getSelectedNotesCountInItem(notebook, selectedItemsList) > 0 + : getSelectedNotesCountInItem(topic, selectedItemsList) > 0 ? "intermediate" : "deselected"; - if (itemState[notebook.id] === "selected") { - contextValue.select(notebook); + if (itemState[topic.id] === "selected") { + contextValue.select(topic); } else { - contextValue.deselect(notebook); - } - for (let topic of notebook.topics) { - itemState[topic.id] = state - ? state - : areAllSelectedItemsInTopic(topic, selectedItemsList) && - getSelectedNotesCountInItem(topic, selectedItemsList) - ? "selected" - : getSelectedNotesCountInItem(topic, selectedItemsList) > 0 - ? "intermediate" - : "deselected"; - if (itemState[topic.id] === "selected") { - contextValue.select(topic); - } else { - contextValue.deselect(topic); - } + contextValue.deselect(topic); } } - return itemState; - }); + } + + useItemSelectionStore.getState().setItemState(itemState); }, [contextValue, getSelectedNotesCountInItem, selectedItemsList] ); @@ -195,32 +196,26 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => { } const updateItemState = useCallback(function (item, state) { - setItemState((itemState) => { - const mergeState = { - [item.id]: state - }; - return { - ...itemState, - ...mergeState - }; + const itemState = useItemSelectionStore.getState().itemState; + const mergeState = { + [item.id]: state + }; + useItemSelectionStore.getState().setItemState({ + ...itemState, + ...mergeState }); }, []); const contextValue = useMemo( () => ({ - enabled: multiSelect, toggleSelection: (item) => { - setItemState((itemState) => { - if (itemState[item.id] === "selected") { - updateItemState(item, "deselected"); - } else { - updateItemState(item, "selected"); - } - - return itemState; - }); + const itemState = useItemSelectionStore.getState().itemState; + if (itemState[item.id] === "selected") { + updateItemState(item, "deselected"); + } else { + updateItemState(item, "selected"); + } }, - setMultiSelect: setMultiSelect, deselect: (item) => { updateItemState(item, "deselected"); }, @@ -231,7 +226,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => { resetItemState(state); } }), - [multiSelect, resetItemState, updateItemState] + [resetItemState, updateItemState] ); const getItemFromId = (id) => { @@ -245,6 +240,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => { const onSave = async () => { const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id); + const itemState = useItemSelectionStore.getState().itemState; for (const id in itemState) { const item = getItemFromId(id); if (itemState[id] === "selected") { @@ -354,7 +350,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => { }} onPress={() => { resetItemState(); - setMultiSelect(false); + useItemSelectionStore.getState().setMultiSelect(false); }} /> @@ -389,13 +385,8 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => { item={item} key={item.id} index={index} - intermediate={itemState[item.id] === "intermediate"} - removed={ - itemState[item.id] === "deselected" && - getSelectedNotesCountInItem(item) > 0 - } + hasNotes={getSelectedNotesCountInItem(item) > 0} sheetRef={actionSheetRef} - isSelected={itemState[item.id] === "selected"} infoText={ <> {item.topics.length === 1 @@ -405,17 +396,14 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => { } getListItems={getItemsForItem} getSublistItemProps={(topic) => ({ - selected: itemState[topic.id] === "selected", - intermediate: itemState[topic.id] === "intermediate", - isSelected: itemState[topic.id] === "selected", - removed: - itemState[topic.id] === "deselected" && - getSelectedNotesCountInItem(topic) > 0, + hasNotes: getSelectedNotesCountInItem(topic) > 0, style: { marginBottom: 0, height: 40 }, onPress: (item) => { + const itemState = + useItemSelectionStore.getState().itemState; const currentState = itemState[item.id]; if (currentState !== "selected") { resetItemState("deselected"); @@ -444,19 +432,22 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => { onAddSublistItem={(item) => { openAddTopicDialog(item); }} + onPress={(item) => { + const itemState = useItemSelectionStore.getState().itemState; + const currentState = itemState[item.id]; + if (currentState !== "selected") { + resetItemState("deselected"); + contextValue.select(item); + } else { + contextValue.deselect(item); + } + }} /> )} itemType="notebook" onAddItem={async (title) => { return await onAddNotebook(title); }} - // ListFooterComponent={ - // - // } /> diff --git a/apps/mobile/app/components/sheets/add-to/list-item.js b/apps/mobile/app/components/sheets/add-to/list-item.js index 7bff8fe29..acebbac79 100644 --- a/apps/mobile/app/components/sheets/add-to/list-item.js +++ b/apps/mobile/app/components/sheets/add-to/list-item.js @@ -27,13 +27,63 @@ import Heading from "../../ui/typography/heading"; import Paragraph from "../../ui/typography/paragraph"; import { useSelectionContext } from "./context"; import { FilteredList } from "./filtered-list"; +import { useItemSelectionStore } from "./store"; -const _ListItem = ({ +const SelectionIndicator = ({ item, hasNotes, selectItem, onPress }) => { + const itemState = useItemSelectionStore((state) => state.itemState[item.id]); + const multiSelect = useItemSelectionStore((state) => state.multiSelect); + + const isSelected = itemState === "selected"; + const isIntermediate = itemState === "intermediate"; + const isRemoved = !isSelected && hasNotes; + const colors = useThemeStore((state) => state.colors); + + return ( + { + if (multiSelect) return selectItem(); + onPress?.(item); + }} + testID={ + isRemoved + ? "close-circle-outline" + : isSelected + ? "check-circle-outline" + : isIntermediate + ? "minus-circle-outline" + : "checkbox-blank-circle-outline" + } + name={ + isRemoved + ? "close-circle-outline" + : isSelected + ? "check-circle-outline" + : isIntermediate + ? "minus-circle-outline" + : "checkbox-blank-circle-outline" + } + /> + ); +}; + +export const ListItem = ({ item, index, icon, infoText, - intermediate, hasSubList, onPress, onScrollEnd, @@ -43,19 +93,21 @@ const _ListItem = ({ sublistItemType, onAddItem, getSublistItemProps, - removed, - isSelected, hasHeaderSearch, onAddSublistItem, + hasNotes, sheetRef }) => { - const { enabled, toggleSelection, setMultiSelect } = useSelectionContext(); + const { toggleSelection } = useSelectionContext(); + const multiSelect = useItemSelectionStore((state) => state.multiSelect); + const colors = useThemeStore((state) => state.colors); const [expanded, setExpanded] = useState(false); function selectItem() { toggleSelection(item); } + return ( { if (hasSubList) return setExpanded(!expanded); - if (enabled) return selectItem(); + if (multiSelect) return selectItem(); onPress?.(item); }} type={type} onLongPress={() => { - setMultiSelect(true); + useItemSelectionStore.getState().setMultiSelect(true); selectItem(); }} customStyle={{ @@ -97,43 +149,11 @@ const _ListItem = ({ alignItems: "center" }} > - { - selectItem(); - if (enabled) return; - onPress?.(item); - }} - testID={ - removed - ? "close-circle-outline" - : isSelected - ? "check-circle-outline" - : intermediate - ? "minus-circle-outline" - : "checkbox-blank-circle-outline" - } - name={ - removed - ? "close-circle-outline" - : isSelected - ? "check-circle-outline" - : intermediate - ? "minus-circle-outline" - : "checkbox-blank-circle-outline" - } + {hasSubList && expanded ? ( @@ -210,11 +230,3 @@ const _ListItem = ({ ); }; - -export const ListItem = React.memo(_ListItem, (prev, next) => { - if (prev.selected === undefined) return false; - if (prev.isSelected !== next.isSelected) return false; - if (prev.selected !== next.selected) return false; - if (prev.intermediate !== next.intermediate) return false; - return true; -}); diff --git a/apps/mobile/app/components/sheets/add-to/store.ts b/apps/mobile/app/components/sheets/add-to/store.ts new file mode 100644 index 000000000..827f47086 --- /dev/null +++ b/apps/mobile/app/components/sheets/add-to/store.ts @@ -0,0 +1,42 @@ +/* +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 . +*/ +import create, { State } from "zustand"; + +type SelectionItemState = Record< + string, + "intermediate" | "selected" | "deselected" +>; + +export interface SelectionStore extends State { + itemState: SelectionItemState; + setItemState: (state: SelectionItemState) => void; + multiSelect: boolean; + setMultiSelect: (multiSelect: boolean) => void; +} + +export const useItemSelectionStore = create((set) => ({ + itemState: {}, + setItemState: (itemState) => { + set({ + itemState + }); + }, + multiSelect: false, + setMultiSelect: (multiSelect) => set({ multiSelect }) +}));