mobile: improve selection performance

This commit is contained in:
ammarahm-ed
2023-03-23 12:22:20 +05:00
committed by Abdullah Atta
parent 47f919cb61
commit 22cd07e414
4 changed files with 167 additions and 126 deletions

View File

@@ -20,13 +20,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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;

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 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);
}}
/>
</View>
@@ -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={
// <View
// style={{
// height: 200
// }}
// />
// }
/>
</SelectionProvider>
</View>

View File

@@ -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 (
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
isRemoved
? colors.red
: isIntermediate || isSelected
? colors.accent
: colors.icon
}
onPress={() => {
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 (
<View
style={{
@@ -67,12 +119,12 @@ const _ListItem = ({
<PressableButton
onPress={() => {
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"
}}
>
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
removed
? colors.red
: intermediate || isSelected
? colors.accent
: colors.icon
}
onPress={() => {
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"
}
<SelectionIndicator
hasNotes={hasNotes}
onPress={onPress}
item={item}
selectItem={selectItem}
/>
<View>
{hasSubList && expanded ? (
@@ -210,11 +230,3 @@ const _ListItem = ({
</View>
);
};
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;
});

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<SelectionStore>((set) => ({
itemState: {},
setItemState: (itemState) => {
set({
itemState
});
},
multiSelect: false,
setMultiSelect: (multiSelect) => set({ multiSelect })
}));