Files
notesnook/apps/mobile/app/screens/move-notes/index.tsx
Kashaf Ansari 10a08d5719 mobile: fix add notes to notebook selection behavior (#10104)
* mobile: fix add notes to notebook selection behavior

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: improve move notes keyboard interactions

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: fix bottom inset when keyboard is open

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-07-30 08:56:18 +05:00

312 lines
9.1 KiB
TypeScript

/*
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 { Note, Notebook, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, FlatList, TextInput, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button";
import { Header } from "../../components/header";
import AppIcon from "../../components/ui/AppIcon";
import Input from "../../components/ui/input";
import { Notice } from "../../components/ui/notice";
import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { useDBItem } from "../../hooks/use-db-item";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation";
import {
createItemSelectionStore,
SelectionState
} from "../../stores/item-selection-store";
import { updateNotebook } from "../../utils/notebooks";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sendItemUpdateEvent } from "../../services/event-manager";
const useItemSelectionStore = createItemSelectionStore(true);
export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
const { colors } = useThemeColors();
const currentNotebook = props.route.params.notebook;
const inputRef = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const hasPendingChanges = useItemSelectionStore((state) => {
return Object.keys(state.selection).some((id) => {
return state.selection[id] !== state.initialState[id];
});
});
useNavigationFocus(props.navigation, { focusOnInit: true });
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const loadNotes = React.useCallback(
async (query?: string) => {
setLoading(true);
const notes = query
? db.lookup.notes(query).sorted()
: db.notes?.all.sorted(db.settings.getGroupOptions("notes") as any);
notes
.then(async (notes) => {
await notes.item(0);
setNotes(notes);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
},
[currentNotebook]
);
useEffect(() => {
loadNotes();
const loadSelection = async () => {
const existingNotes = await db.relations
.from(currentNotebook, "note")
.get();
const selection: Record<string, SelectionState> = {};
existingNotes.forEach((rel) => {
selection[rel.toId] = "selected";
});
useItemSelectionStore.setState({
selection,
initialState: selection
});
};
loadSelection();
return () => {
useItemSelectionStore.getState().reset();
};
}, [currentNotebook, loadNotes]);
const renderItem = React.useCallback(
({ index }: { item: boolean; index: number }) => {
return <SelectableNoteItem id={index} items={notes} />;
},
[notes]
);
return (
<SafeAreaView
style={{
gap: DefaultAppStyles.GAP_VERTICAL,
flex: 1,
backgroundColor: colors.primary.background
}}
>
<Header title={strings.addNotes()} canGoBack />
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
flexGrow: 1,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Input
button={{
icon: "magnify",
color: colors.primary.icon,
size: AppFontSize.lg,
onPress: () => {}
}}
testID="search-input"
fwdRef={inputRef}
wrapperStyle={{
marginBottom: 0,
flexShrink: 1
}}
flexGrow={0}
containerStyle={{
flexGrow: 0
}}
autoCapitalize="none"
onChangeText={(v) => {
loadNotes(v && v.trim() === "" ? undefined : v.trim());
}}
placeholder={strings.searchANote()}
/>
<Notice
text={strings.linkingNotesTo(currentNotebook.title)}
size="small"
type="information"
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
/>
<FlatList
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
ListEmptyComponent={
<View
style={{
minHeight: 100,
justifyContent: "center",
alignItems: "center"
}}
>
{loading ? (
<ActivityIndicator size="large" color={colors.primary.accent} />
) : (
<Paragraph color={colors.secondary.paragraph}>
{strings.emptyPlaceholders("note")}
</Paragraph>
)}
</View>
}
style={{ flex: 1 }}
contentContainerStyle={{
paddingBottom: hasPendingChanges ? 80 : 0
}}
data={loading ? [] : notes?.placeholders}
renderItem={renderItem}
/>
{hasPendingChanges ? (
<FloatingButton
icon="check"
alwaysVisible
hideOnKeyboard={false}
onPress={async () => {
await db.notes?.addToNotebook(
currentNotebook.id,
...useItemSelectionStore.getState().getSelectedItemIds()
);
await db.notes?.removeFromNotebook(
currentNotebook.id,
...useItemSelectionStore.getState().getDeselectedItemIds()
);
updateNotebook(currentNotebook.id);
sendItemUpdateEvent(currentNotebook.id, "notebook");
Navigation.queueRoutesForUpdate();
Navigation.goBack();
}}
/>
) : null}
</View>
</SafeAreaView>
);
};
const SelectableNoteItem = React.memo(
({
id,
items
}: {
id: string | number;
items?: VirtualizedGrouping<Note>;
}) => {
const { colors } = useThemeColors();
const [item] = useDBItem(id, "note", items);
const selected = useItemSelectionStore((state) =>
item?.id ? state.selection[item.id] === "selected" : false
);
const exists = useItemSelectionStore((state) =>
item?.id ? state.initialState[item.id] === "selected" : false
);
return (
<Pressable
testID="listitem.select"
onPress={() => {
if (!item) return;
useItemSelectionStore
.getState()
.markAs(item, selected ? "deselected" : "selected");
}}
type={"transparent"}
style={{
flexDirection: "row",
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
{!item ? null : (
<>
<View
style={{
flexGrow: 1
}}
>
<Heading
size={AppFontSize.sm}
numberOfLines={1}
style={{
maxWidth: "93%"
}}
>
{item?.title}
</Heading>
{item.type == "note" && item.headline ? (
<Paragraph
numberOfLines={1}
color={colors?.secondary.paragraph}
size={AppFontSize.sm}
style={{
maxWidth: "93%"
}}
>
{item.headline}
</Paragraph>
) : null}
</View>
<AppIcon
style={{
backgroundColor: "transparent"
}}
size={AppFontSize.lg}
name={selected ? "checkbox-outline" : "checkbox-blank-outline"}
color={
selected
? colors.selected.icon
: exists && !selected
? colors.static.red
: colors.primary.icon
}
/>
</>
)}
</Pressable>
);
}
);
SelectableNoteItem.displayName = "SelectableNoteItem";
MoveNotes.present = (notebook?: Notebook) => {};
export default MoveNotes;