mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
* 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>
312 lines
9.1 KiB
TypeScript
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;
|