mobile: use index based grouping

This commit is contained in:
Ammar Ahmed
2023-12-08 10:13:43 +05:00
parent d65917c85b
commit ed81319d2c
17 changed files with 504 additions and 388 deletions

View File

@@ -18,22 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { formatBytes } from "@notesnook/common";
import React, { useEffect, useState } from "react";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useThemeColors } from "@notesnook/theme";
import { useDBItem } from "../../hooks/use-db-item";
import { SIZE } from "../../utils/size";
import { IconButton } from "../ui/icon-button";
import { ProgressCircleComponent } from "../ui/svg/lazy";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { useDBItem } from "../../hooks/use-db-item";
function getFileExtension(filename: string) {
var ext = /^.+\.([^.]+)$/.exec(filename);
const ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
@@ -46,7 +46,7 @@ export const AttachmentItem = ({
hideWhenNotDownloading,
context
}: {
id: string;
id: string | number;
attachments?: VirtualizedGrouping<Attachment>;
encryption?: boolean;
setAttachments: (attachments: any) => void;
@@ -54,7 +54,7 @@ export const AttachmentItem = ({
hideWhenNotDownloading?: boolean;
context?: string;
}) => {
const [attachment] = useDBItem(id, "attachment", attachments?.item);
const [attachment] = useDBItem(id, "attachment", attachments);
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
@@ -68,7 +68,7 @@ export const AttachmentItem = ({
};
return (hideWhenNotDownloading &&
(!currentProgress || !(currentProgress as any).value)) ||
(!currentProgress || !currentProgress.value)) ||
!attachment ? null : (
<TouchableOpacity
activeOpacity={0.9}
@@ -133,8 +133,8 @@ export const AttachmentItem = ({
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.size)}{" "}
{(currentProgress as any)?.type
? "(" + (currentProgress as any).type + "ing - tap to cancel)"
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}

View File

@@ -91,7 +91,7 @@ const DownloadAttachments = ({
const successResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
for (const [key, value] of result.entries()) {
if (value.status === 1) results.push(db.attachments.attachment(key));
}
return results;
@@ -99,7 +99,7 @@ const DownloadAttachments = ({
const failedResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
for (const [key, value] of result.entries()) {
if (value.status === 0) results.push(db.attachments.attachment(key));
}
return results;
@@ -207,11 +207,11 @@ const DownloadAttachments = ({
</Paragraph>
</View>
}
keyExtractor={(item) => item as string}
renderItem={({ item }) => {
keyExtractor={(item, index) => "attachment" + index}
renderItem={({ index }) => {
return (
<AttachmentItem
id={item as string}
id={index}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}

View File

@@ -83,14 +83,14 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(async () => {
let results = await db.lookup.attachments(
attachmentSearchValue.current as string
);
const results = await db.lookup
.attachments(attachmentSearchValue.current as string)
.sorted();
setAttachments(results);
}, 300);
};
const renderItem = ({ item }: { item: string }) => (
const renderItem = ({ item }: { item: string | number }) => (
<AttachmentItem
setAttachments={setAttachments}
attachments={attachments}
@@ -102,15 +102,15 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
const onCheck = async () => {
if (!attachments) return;
setLoading(true);
for (let id of attachments.ids) {
const attachment = await attachments.item(id as string);
for (const id of attachments.ids) {
const attachment = (await attachments.item(id))?.item;
if (!attachment) continue;
let result = await filesystem.checkAttachment(attachment.hash);
const result = await filesystem.checkAttachment(attachment.hash);
if (result.failed) {
await db.attachments.markAsFailed(attachment.hash, result.failed);
} else {
await db.attachments.markAsFailed(id as string, undefined);
await db.attachments.markAsFailed(attachment.id, undefined);
}
}
refresh();
@@ -306,7 +306,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
/>
}
estimatedItemSize={50}
data={attachments?.ids as string[]}
data={attachments?.ids}
renderItem={renderItem}
/>

View File

@@ -51,19 +51,18 @@ const JumpToSectionDialog = () => {
const notes = data;
const [visible, setVisible] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const currentScrollPosition = useRef(0);
const [groups, setGroups] = useState<
{
index: number;
group: GroupHeader;
}[]
>();
const offsets = useRef<number[]>([]);
const timeout = useRef<NodeJS.Timeout>();
const onPress = (item: GroupHeader) => {
const index = notes?.ids?.findIndex((i) => {
if (typeof i === "object") {
return i.title === item.title && i.type === "header";
} else {
false;
}
});
const onPress = (item: { index: number; group: GroupHeader }) => {
scrollRef.current?.current?.scrollToIndex({
index: index as number,
index: item.index,
animated: true
});
close();
@@ -97,55 +96,44 @@ const JumpToSectionDialog = () => {
}, [open]);
const onScroll = (data: { x: number; y: number }) => {
const y = data.y;
if (timeout) {
clearTimeout(timeout.current);
timeout.current = undefined;
}
timeout.current = setTimeout(() => {
setCurrentIndex(
offsets.current?.findIndex(
(o, i) => o <= y && offsets.current[i + 1] > y
) || 0
);
}, 200);
currentScrollPosition.current = data.y;
};
const close = () => {
setVisible(false);
};
const loadOffsets = useCallback(() => {
notes?.ids
.filter((i) => typeof i === "object" && i.type === "header")
.map((item, index) => {
if (typeof item === "string") return;
const loadGroupsAndOffsets = useCallback(() => {
notes?.groups?.().then((groups) => {
setGroups(groups);
offsets.current = [];
groups.map((item, index) => {
let offset = 35 * index;
let ind = notes.ids.findIndex(
(i) =>
typeof i === "object" &&
i.title === item.title &&
i.type === "header"
);
let groupIndex = item.index;
const messageState = useMessageStore.getState().message;
const msgOffset = messageState?.visible ? 60 : 10;
ind = ind + 1;
ind = ind - (index + 1);
offset = offset + ind * 100 + msgOffset;
groupIndex = groupIndex + 1;
groupIndex = groupIndex - (index + 1);
offset = offset + groupIndex * 100 + msgOffset;
offsets.current.push(offset);
});
}, [notes]);
useEffect(() => {
loadOffsets();
}, [loadOffsets, notes]);
const index = offsets.current?.findIndex((o, i) => {
return (
o <= currentScrollPosition.current + 100 &&
offsets.current[i + 1] - 100 > currentScrollPosition.current
);
});
setCurrentIndex(index < 0 ? 0 : index);
});
}, [notes]);
return !visible ? null : (
<BaseDialog
onShow={() => {
loadOffsets();
loadGroupsAndOffsets();
}}
onRequestClose={close}
visible={true}
@@ -178,40 +166,38 @@ const JumpToSectionDialog = () => {
paddingBottom: 20
}}
>
{notes?.ids
.filter((i) => typeof i === "object" && i.type === "header")
.map((item, index) => {
return typeof item === "object" && item.title ? (
<PressableButton
key={item.title}
onPress={() => onPress(item)}
type={currentIndex === index ? "selected" : "transparent"}
customStyle={{
minWidth: "20%",
width: null,
paddingHorizontal: 12,
margin: 5,
borderRadius: 100,
height: 25,
marginVertical: 10
{groups?.map((item, index) => {
return (
<PressableButton
key={item.group.id}
onPress={() => onPress(item)}
type={currentIndex === index ? "selected" : "transparent"}
customStyle={{
minWidth: "20%",
width: null,
paddingHorizontal: 12,
margin: 5,
borderRadius: 100,
height: 25,
marginVertical: 10
}}
>
<Paragraph
size={SIZE.sm}
color={
currentIndex === index
? colors.selected.accent
: colors.primary.accent
}
style={{
textAlign: "center"
}}
>
<Paragraph
size={SIZE.sm}
color={
currentIndex === index
? colors.selected.accent
: colors.primary.accent
}
style={{
textAlign: "center"
}}
>
{item.title}
</Paragraph>
</PressableButton>
) : null;
})}
{item.group.title}
</Paragraph>
</PressableButton>
);
})}
</View>
</ScrollView>
</View>

View File

@@ -17,13 +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 {
GroupHeader,
GroupingKey,
Item,
VirtualizedGrouping,
isGroupHeader
} from "@notesnook/core";
import { GroupingKey, Item, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { FlashList } from "@shopify/flash-list";
import React, { useEffect, useRef } from "react";
@@ -39,11 +33,10 @@ import { eSendEvent } from "../../services/event-manager";
import Sync from "../../services/sync";
import { RouteName } from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { eOpenJumpToDialog, eScrollEvent } from "../../utils/events";
import { eScrollEvent } from "../../utils/events";
import { tabBarRef } from "../../utils/global-refs";
import { Footer } from "../list-items/footer";
import { Header } from "../list-items/headers/header";
import { SectionHeader } from "../list-items/headers/section-header";
import { Empty, PlaceholderData } from "./empty";
import { ListItemWrapper } from "./list-item.wrapper";
@@ -92,35 +85,20 @@ export default function List(props: ListProps) {
};
const renderItem = React.useCallback(
({ item, index }: { item: string | GroupHeader; index: number }) => {
if (isGroupHeader(item)) {
return (
<SectionHeader
screen={props.renderedInRoute}
item={item}
index={index}
dataType={props.dataType}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: scrollRef,
data: props.data
});
}}
/>
);
} else {
return (
<ListItemWrapper
id={item}
index={index}
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
group={groupType as GroupingKey}
/>
);
}
({ index }: { index: number }) => {
return (
<ListItemWrapper
index={index}
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupOptions={groupOptions}
group={groupType as GroupingKey}
renderedInRoute={props.renderedInRoute}
customAccentColor={props.customAccentColor}
dataType={props.dataType}
scrollRef={scrollRef}
/>
);
},
[
groupOptions,
@@ -181,7 +159,6 @@ export default function List(props: ListProps) {
onMomentumScrollEnd={() => {
tabBarRef.current?.unlock();
}}
getItemType={(item: any) => (isGroupHeader(item) ? "header" : "item")}
estimatedItemSize={isCompactModeEnabled ? 60 : 100}
directionalLockEnabled={true}
keyboardShouldPersistTaps="always"

View File

@@ -16,24 +16,29 @@ 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 React from "react";
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
import {
Color,
GroupHeader,
GroupOptions,
GroupingKey,
Item,
VirtualizedGrouping,
Color,
Reminder,
ItemType,
Note,
Notebook,
Reminder,
Tag,
TrashItem,
ItemType,
Note
VirtualizedGrouping
} from "@notesnook/core";
import { useEffect, useRef, useState } from "react";
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { NoteWrapper } from "../list-items/note/wrapper";
import { db } from "../../common/database";
import { eSendEvent } from "../../services/event-manager";
import { RouteName } from "../../stores/use-navigation-store";
import { eOpenJumpToDialog } from "../../utils/events";
import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
@@ -45,13 +50,17 @@ export type TagsWithDateEdited = WithDateEdited<Tag>;
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
id: string;
isSheet: boolean;
index: number;
renderedInRoute?: RouteName;
customAccentColor?: string;
dataType: string;
scrollRef: any;
groupOptions: GroupOptions;
};
export function ListItemWrapper(props: ListItemWrapperProps) {
const { id, items, group, isSheet, index } = props;
const { items, group, isSheet, index, groupOptions } = props;
const [item, setItem] = useState<Item>();
const tags = useRef<TagsWithDateEdited>();
const notebooks = useRef<NotebooksWithDateEdited>();
@@ -59,25 +68,34 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
const color = useRef<Color>();
const totalNotes = useRef<number>(0);
const attachmentsCount = useRef(0);
const [groupHeader, setGroupHeader] = useState<GroupHeader>();
const previousIndex = useRef<number>();
useEffect(() => {
(async function () {
const { item, data } = (await items?.item(id, resolveItems)) || {};
if (!item) return;
if (item.type === "note" && isNoteResolvedData(data)) {
tags.current = data.tags;
notebooks.current = data.notebooks;
reminder.current = data.reminder;
color.current = data.color;
attachmentsCount.current = data.attachmentsCount;
} else if (item.type === "notebook" && typeof data === "number") {
totalNotes.current = data;
} else if (item.type === "tag" && typeof data === "number") {
totalNotes.current = data;
try {
const { item, data, group } =
(await items?.item(index, resolveItems)) || {};
if (!item) return;
if (item.type === "note" && isNoteResolvedData(data)) {
tags.current = data.tags;
notebooks.current = data.notebooks;
reminder.current = data.reminder;
color.current = data.color;
attachmentsCount.current = data.attachmentsCount;
} else if (item.type === "notebook" && typeof data === "number") {
totalNotes.current = data;
} else if (item.type === "tag" && typeof data === "number") {
totalNotes.current = data;
}
previousIndex.current = index;
setItem(item);
setGroupHeader(group);
} catch (e) {
console.log("Error", e);
}
setItem(item);
})();
}, [id, items]);
}, [index, items]);
if (!item) return <View style={{ height: 100, width: "100%" }} />;
@@ -85,40 +103,117 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
switch (type) {
case "note": {
return (
<NoteWrapper
item={item as Note}
tags={tags.current}
color={color.current}
notebooks={notebooks.current}
reminder={reminder.current}
attachmentsCount={attachmentsCount.current}
date={getDate(item, group)}
isRenderedInActionSheet={isSheet}
index={index}
/>
<>
{groupHeader && previousIndex.current === index ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<NoteWrapper
item={item as Note}
tags={tags.current}
color={color.current}
notebooks={notebooks.current}
reminder={reminder.current}
attachmentsCount={attachmentsCount.current}
date={getDate(item, group)}
isRenderedInActionSheet={isSheet}
index={index}
/>
</>
);
}
case "notebook":
return (
<NotebookWrapper
item={item as Notebook}
totalNotes={totalNotes.current}
date={getDate(item, group)}
index={index}
/>
<>
{groupHeader && previousIndex.current === index ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<NotebookWrapper
item={item as Notebook}
totalNotes={totalNotes.current}
date={getDate(item, group)}
index={index}
/>
</>
);
case "reminder":
return (
<ReminderItem item={item as Reminder} index={index} isSheet={isSheet} />
<>
{groupHeader && previousIndex.current === index ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<ReminderItem
item={item as Reminder}
index={index}
isSheet={isSheet}
/>
</>
);
case "tag":
return (
<TagItem
item={item as Tag}
index={index}
totalNotes={totalNotes.current}
/>
<>
{groupHeader && previousIndex.current === index ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<TagItem
item={item as Tag}
index={index}
totalNotes={totalNotes.current}
/>
</>
);
default:
return null;
@@ -136,6 +231,19 @@ function withDateEdited<
return { dateEdited: latestDateEdited, items };
}
export async function resolveItems(ids: string[], items: Item[]) {
const { type } = items[0];
if (type === "note") return resolveNotes(ids);
else if (type === "notebook") {
return Promise.all(ids.map((id) => db.notebooks.totalNotes(id)));
} else if (type === "tag") {
return Promise.all(
ids.map((id) => db.relations.from({ id, type: "tag" }, "note").count())
);
}
return [];
}
function getDate(item: Item, groupType?: GroupingKey): number {
return (
getSortValue(
@@ -151,22 +259,6 @@ function getDate(item: Item, groupType?: GroupingKey): number {
);
}
async function resolveItems(ids: string[], items: Record<string, Item>) {
const { type } = items[ids[0]];
if (type === "note") return resolveNotes(ids);
else if (type === "notebook") {
const data: Record<string, number> = {};
for (const id of ids) data[id] = await db.notebooks.totalNotes(id);
return data;
} else if (type === "tag") {
const data: Record<string, number> = {};
for (const id of ids)
data[id] = await db.relations.from({ id, type: "tag" }, "note").count();
return data;
}
return {};
}
type NoteResolvedData = {
notebooks?: NotebooksWithDateEdited;
reminder?: Reminder;
@@ -183,7 +275,6 @@ async function resolveNotes(ids: string[]) {
...(await db.relations.from({ type: "note", ids }, "reminder").get())
];
console.timeEnd("relations");
const relationIds: {
notebooks: Set<string>;
colors: Set<string>;
@@ -207,15 +298,15 @@ async function resolveNotes(ids: string[]) {
> = {};
for (const relation of relations) {
const noteId =
relation.toType === "relation" ? relation.fromId : relation.toId;
relation.toType === "reminder" ? relation.fromId : relation.toId;
const data = grouped[noteId] || {
notebooks: [],
tags: []
};
if (relation.toType === "relation" && !data.reminder) {
data.reminder = relation.fromId;
relationIds.reminders.add(relation.fromId);
if (relation.toType === "reminder" && !data.reminder) {
data.reminder = relation.toId;
relationIds.reminders.add(relation.toId);
} else if (relation.fromType === "notebook" && data.notebooks.length < 2) {
data.notebooks.push(relation.fromId);
relationIds.notebooks.add(relation.fromId);
@@ -226,7 +317,7 @@ async function resolveNotes(ids: string[]) {
data.color = relation.fromId;
relationIds.colors.add(relation.fromId);
}
grouped[relation.toId] = data;
grouped[noteId] = data;
}
console.time("resolve");
@@ -240,10 +331,10 @@ async function resolveNotes(ids: string[]) {
};
console.timeEnd("resolve");
const data: Record<string, NoteResolvedData> = {};
const data: NoteResolvedData[] = [];
for (const noteId in grouped) {
const group = grouped[noteId];
data[noteId] = {
data.push({
color: group.color ? resolved.colors[group.color] : undefined,
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
tags: withDateEdited(group.tags.map((id) => resolved.tags[id])),
@@ -252,12 +343,12 @@ async function resolveNotes(ids: string[]) {
),
attachmentsCount:
(await db.attachments?.ofNote(noteId, "all").ids())?.length || 0
};
});
}
return data;
}
function isNoteResolvedData(data: unknown): data is NoteResolvedData {
export function isNoteResolvedData(data: unknown): data is NoteResolvedData {
return (
typeof data === "object" &&
!!data &&

View File

@@ -120,13 +120,15 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
<Line bottom={0} />
{item.type === "note" ? <Tags close={close} item={item} /> : null}
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
{item.type === "note" ? (
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
) : null}
<Items
item={item}

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 { GroupHeader, Note } from "@notesnook/core";
import { Note } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useCallback, useEffect } from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
@@ -135,10 +135,9 @@ const MoveNoteSheet = ({
};
const renderNotebook = useCallback(
({ item, index }: { item: string | GroupHeader; index: number }) =>
(item as GroupHeader).type === "header" ? null : (
<NotebookItem items={notebooks} id={item as string} index={index} />
),
({ item, index }: { item: string | number; index: number }) => (
<NotebookItem items={notebooks} id={item as string} index={index} />
),
[notebooks]
);
@@ -220,12 +219,11 @@ const MoveNoteSheet = ({
}}
>
<FlashList
data={notebooks?.ids?.filter((id) => typeof id === "string")}
data={notebooks?.ids}
style={{
width: "100%"
}}
estimatedItemSize={50}
keyExtractor={(item) => item as string}
renderItem={renderNotebook}
ListEmptyComponent={
<View

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useMemo } from "react";
import React, { useEffect } from "react";
import { View, useWindowDimensions } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { useTotalNotes } from "../../../hooks/use-db-item";
@@ -46,31 +46,39 @@ export const NotebookItem = ({
parent,
items
}: {
id: string;
id: string | number;
currentLevel?: number;
index: number;
parent?: NotebookParentProp;
items?: VirtualizedGrouping<Notebook>;
}) => {
const { nestedNotebooks, notebook: item } = useNotebook(id, items);
const ids = useMemo(() => (id ? [id] : []), [id]);
const { totalNotes: totalNotes } = useTotalNotes(ids, "notebook");
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);
const { nestedNotebooks, notebook: item } = useNotebook(id, items, expanded);
const { totalNotes: totalNotes, getTotalNotes } = useTotalNotes("notebook");
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
const { colors } = useThemeColors("sheet");
const selection = useNotebookItemSelectionStore((state) =>
id ? state.selection[id] : undefined
item?.id ? state.selection[item?.id] : undefined
);
const isSelected = selection === "selected";
const isFocused = focusedRouteId === id;
const { fontScale } = useWindowDimensions();
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);
useEffect(() => {
if (item?.id) {
getTotalNotes([item?.id]);
}
}, [getTotalNotes, item?.id]);
const onPress = () => {
if (!item) return;
const state = useNotebookItemSelectionStore.getState();
if (isSelected) {
state.markAs(item, !state.initialState[id] ? undefined : "deselected");
state.markAs(
item,
!state.initialState[item?.id] ? undefined : "deselected"
);
return;
}
@@ -112,7 +120,7 @@ export const NotebookItem = ({
item,
!isSelected
? "selected"
: !state.initialState[id]
: !state.initialState[item?.id]
? undefined
: "deselected"
);
@@ -141,7 +149,8 @@ export const NotebookItem = ({
size={SIZE.xl}
color={isSelected ? colors.selected.icon : colors.primary.icon}
onPress={() => {
useNotebookExpandedStore.getState().setExpanded(id);
if (!item?.id) return;
useNotebookExpandedStore.getState().setExpanded(item?.id);
}}
top={0}
left={0}
@@ -201,9 +210,9 @@ export const NotebookItem = ({
alignItems: "center"
}}
>
{totalNotes?.(id) ? (
{item?.id && totalNotes?.(item?.id) ? (
<Paragraph size={SIZE.sm} color={colors.secondary.paragraph}>
{totalNotes(id)}
{totalNotes(item?.id)}
</Paragraph>
) : null}
<IconButton
@@ -231,8 +240,8 @@ export const NotebookItem = ({
? null
: nestedNotebooks?.ids.map((id, index) => (
<NotebookItem
key={id as string}
id={id as string}
key={item?.id + "_" + id}
id={id}
index={index}
currentLevel={currentLevel + 1}
items={nestedNotebooks}

View File

@@ -115,7 +115,7 @@ const ManageTagsSheet = (props: {
});
} else {
db.tags.all.sorted(db.settings.getGroupOptions("tags")).then((items) => {
console.log("items loaded tags");
console.log("items loaded tags", items.ids);
setTags(items);
});
}
@@ -237,11 +237,10 @@ const ManageTagsSheet = (props: {
);
const renderTag = useCallback(
({ item }: { item: string; index: number }) => (
({ item, index }: { item: string | number; index: number }) => (
<TagItem
key={item as string}
tags={tags as VirtualizedGrouping<Tag>}
id={item as string}
id={index}
onPress={onPress}
/>
),
@@ -303,13 +302,13 @@ const ManageTagsSheet = (props: {
) : null}
<FlatList
data={tags?.ids?.filter((id) => typeof id === "string") as string[]}
data={tags?.ids}
style={{
width: "100%"
}}
keyboardShouldPersistTaps
keyboardDismissMode="interactive"
keyExtractor={(item) => item as string}
keyExtractor={(item) => item + "_tag"}
renderItem={renderTag}
ListEmptyComponent={
<View
@@ -352,29 +351,38 @@ const TagItem = ({
tags,
onPress
}: {
id: string;
id: string | number;
tags: VirtualizedGrouping<Tag>;
onPress: (id: string) => void;
}) => {
const { colors } = useThemeColors();
const [tag] = useDBItem(id, "tag", tags);
const selection = useTagItemSelection((state) => state.selection[id]);
const selection = useTagItemSelection((state) =>
tag?.id ? state.selection[tag?.id] : false
);
return (
return !tag ? null : (
<PressableButton
key={tag?.id}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "flex-start",
height: 40
}}
onPress={() => onPress(id)}
onPress={() => {
if (!tag) return;
onPress(tag.id);
}}
type="gray"
>
{!tag ? null : (
<Icon
size={22}
onPress={() => onPress(id)}
onPress={() => {
if (!tag) return;
onPress(tag.id);
}}
color={
selection === "selected" || selection === "intermediate"
? colors.selected.icon
@@ -406,7 +414,6 @@ const TagItem = ({
style={{
width: 200,
height: 30,
// backgroundColor: colors.secondary.background,
borderRadius: 5
}}
/>

View File

@@ -36,6 +36,7 @@ import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { useDBItem } from "../../../hooks/use-db-item";
export const MoveNotes = ({
notebook,
@@ -45,11 +46,13 @@ export const MoveNotes = ({
fwdRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const [currentNotebook, setCurrentNotebook] = useState(notebook);
const currentNotebook = notebook;
const { height } = useWindowDimensions();
const [selectedNoteIds, setSelectedNoteIds] = useState<string[]>([]);
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const [existingNoteIds, setExistingNoteIds] = useState<string[]>([]);
useEffect(() => {
db.notes?.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
setNotes(notes);
@@ -85,17 +88,18 @@ export const MoveNotes = ({
);
const renderItem = React.useCallback(
({ item }: { item: string }) => {
({ index }: { item: string | number; index: number }) => {
return (
<SelectableNoteItem
id={item}
id={index}
items={notes}
select={select}
selected={selectedNoteIds?.indexOf(item) > -1}
selected={(id) => selectedNoteIds?.indexOf(id) > -1}
exists={(id) => existingNoteIds.indexOf(id) > -1}
/>
);
},
[notes, select, selectedNoteIds]
[existingNoteIds, notes, select, selectedNoteIds]
);
return (
@@ -128,9 +132,7 @@ export const MoveNotes = ({
</Paragraph>
</View>
}
data={(notes?.ids as string[])?.filter(
(id) => existingNoteIds?.indexOf(id) === -1
)}
data={notes?.ids}
renderItem={renderItem}
/>
{selectedNoteIds.length > 0 ? (
@@ -157,21 +159,19 @@ const SelectableNoteItem = ({
id,
items,
select,
selected
selected,
exists
}: {
id: string;
id: string | number;
items?: VirtualizedGrouping<Note>;
select: (id: string) => void;
selected?: boolean;
selected?: (id: string) => boolean;
exists: (id: string) => boolean;
}) => {
const { colors } = useThemeColors();
const [item, setItem] = useState<Note>();
const [item] = useDBItem(id, "note", items);
useEffect(() => {
items?.item(id).then((item) => setItem(item));
}, [id, items]);
return !item ? null : (
return !item || exists(item.id) ? null : (
<PressableButton
testID="listitem.select"
onPress={() => {
@@ -197,10 +197,14 @@ const SelectableNoteItem = ({
select(item?.id);
}}
name={
selected ? "check-circle-outline" : "checkbox-blank-circle-outline"
selected?.(item?.id)
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
type="selected"
color={selected ? colors.selected.icon : colors.primary.icon}
color={
selected?.(item?.id) ? colors.selected.icon : colors.primary.icon
}
/>
<View

View File

@@ -123,7 +123,11 @@ export const NotebookSheet = () => {
nestedNotebooks: notebooks,
nestedNotebookNotesCount: totalNotes,
groupOptions
} = useNotebook(currentRoute === "Notebook" ? root : undefined);
} = useNotebook(
currentRoute === "Notebook" ? root : undefined,
undefined,
true
);
const PLACEHOLDER_DATA = {
heading: "Notebooks",
@@ -140,17 +144,16 @@ export const NotebookSheet = () => {
item,
index
}: {
item: string | GroupHeader;
item: string | number;
index: number;
}) =>
(item as GroupHeader).type === "header" ? null : (
<NotebookItem
items={notebooks}
id={item as string}
index={index}
totalNotes={totalNotes}
/>
);
}) => (
<NotebookItem
items={notebooks}
id={item as string}
index={index}
totalNotes={totalNotes}
/>
);
const selectionContext = {
selection: selection,
@@ -396,7 +399,6 @@ export const NotebookSheet = () => {
progressBackgroundColor={colors.primary.background}
/>
}
keyExtractor={(item) => item as string}
renderItem={renderNotebook}
ListEmptyComponent={
<View
@@ -426,7 +428,7 @@ const NotebookItem = ({
parent,
items
}: {
id: string;
id: string | number;
totalNotes: (id: string) => number;
currentLevel?: number;
index: number;
@@ -437,7 +439,7 @@ const NotebookItem = ({
nestedNotebookNotesCount,
nestedNotebooks,
notebook: item
} = useNotebook(id, items);
} = useNotebook(id, items, true);
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
const { colors } = useThemeColors("sheet");
const selection = useSelection();
@@ -445,7 +447,9 @@ const NotebookItem = ({
selection.selection.findIndex((selected) => selected.id === item?.id) > -1;
const { fontScale } = useWindowDimensions();
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);
const expanded = useNotebookExpandedStore((state) =>
item?.id ? state.expanded[item?.id] : undefined
);
return (
<View
@@ -511,7 +515,8 @@ const NotebookItem = ({
size={SIZE.lg}
color={isSelected ? colors.selected.icon : colors.primary.icon}
onPress={() => {
useNotebookExpandedStore.getState().setExpanded(id);
if (!item?.id) return;
useNotebookExpandedStore.getState().setExpanded(item?.id);
}}
top={0}
left={0}
@@ -553,9 +558,9 @@ const NotebookItem = ({
alignItems: "center"
}}
>
{totalNotes?.(id) ? (
{item?.id && totalNotes?.(item?.id) ? (
<Paragraph size={SIZE.sm} color={colors.secondary.paragraph}>
{totalNotes(id)}
{totalNotes(item?.id)}
</Paragraph>
) : null}
<IconButton
@@ -580,10 +585,11 @@ const NotebookItem = ({
{!expanded
? null
: nestedNotebooks?.ids.map((id, index) => (
: item &&
nestedNotebooks?.ids.map((id, index) => (
<NotebookItem
key={id as string}
id={id as string}
key={item.id + "_" + id}
id={index}
index={index}
totalNotes={nestedNotebookNotesCount}
currentLevel={currentLevel + 1}

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useEffect, useState } from "react";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { Attachment } from "@notesnook/core";
type AttachmentProgress = {
type: string;
@@ -27,7 +28,7 @@ type AttachmentProgress = {
};
export const useAttachmentProgress = (
attachment: any,
attachment?: Attachment,
encryption?: boolean
): [
AttachmentProgress | undefined,
@@ -45,7 +46,9 @@ export const useAttachmentProgress = (
);
useEffect(() => {
const attachmentProgress = progress?.[attachment?.metadata?.hash];
const attachmentProgress = !attachment
? null
: progress?.[attachment?.hash];
if (attachmentProgress) {
const type = attachmentProgress.type;
const loaded =

View File

@@ -26,7 +26,7 @@ import {
Tag,
VirtualizedGrouping
} from "@notesnook/core";
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { db } from "../common/database";
import {
eSendEvent,
@@ -45,22 +45,36 @@ type ItemTypeKey = {
shortcut: Shortcut;
};
function isValidIdOrIndex(idOrIndex?: string | number) {
return typeof idOrIndex === "number" || typeof idOrIndex === "string";
}
export const useDBItem = <T extends keyof ItemTypeKey>(
id?: string,
idOrIndex?: string | number,
type?: T,
items?: VirtualizedGrouping<ItemTypeKey[T]>
): [ItemTypeKey[T] | undefined, () => void] => {
const [item, setItem] = useState<ItemTypeKey[T]>();
const itemIdRef = useRef<string>();
const prevIdOrIndexRef = useRef<string | number>();
if (prevIdOrIndexRef.current !== idOrIndex) {
itemIdRef.current = undefined;
prevIdOrIndexRef.current = idOrIndex;
}
useEffect(() => {
const onUpdateItem = (itemId?: string) => {
if (typeof itemId === "string" && itemId !== id) return;
if (!id) return;
console.log("useDBItem.onUpdateItem", id, type);
if (typeof itemId === "string" && itemId !== itemIdRef.current) return;
if (items) {
items.item(id).then((item) => {
setItem(item);
if (!isValidIdOrIndex(idOrIndex)) return;
console.log("useDBItem.onUpdateItem", idOrIndex, type);
if (items && typeof idOrIndex === "number") {
items.item(idOrIndex).then((item) => {
setItem(item.item);
itemIdRef.current = item.item.id;
});
} else {
if (!(db as any)[type + "s"][type]) {
@@ -69,9 +83,14 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
`db.${type}s.${type}(id: string)`
);
} else {
console.log("get notebook");
(db as any)[type + "s"]
?.[type]?.(id)
.then((item: ItemTypeKey[T]) => setItem(item));
?.[type]?.(idOrIndex as string)
.then((item: ItemTypeKey[T]) => {
setItem(item);
itemIdRef.current = item.id;
});
}
}
};
@@ -80,46 +99,42 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
return () => {
eUnSubscribeEvent(eDBItemUpdate, onUpdateItem);
};
}, [id, type, items]);
}, [idOrIndex, type, items]);
return [
id ? (item as ItemTypeKey[T]) : undefined,
isValidIdOrIndex(idOrIndex) ? (item as ItemTypeKey[T]) : undefined,
() => {
if (id) {
eSendEvent(eDBItemUpdate, id);
if (idOrIndex) {
eSendEvent(eDBItemUpdate, itemIdRef.current || idOrIndex);
}
}
];
};
export const useTotalNotes = (
ids: string[],
type: "notebook" | "tag" | "color"
) => {
export const useTotalNotes = (type: "notebook" | "tag" | "color") => {
const [totalNotesById, setTotalNotesById] = useState<{
[id: string]: number;
}>({});
const getTotalNotes = React.useCallback(() => {
if (!ids || !ids.length || !type) return;
db.relations
.from({ type: "notebook", ids: ids as string[] }, ["notebook", "note"])
.get()
.then((relations) => {
const totalNotesById: any = {};
for (const id of ids) {
totalNotesById[id] = relations.filter(
(relation) => relation.fromId === id && relation.toType === "note"
)?.length;
}
setTotalNotesById(totalNotesById);
});
console.log("useTotalNotes.getTotalNotes");
}, [ids, type]);
useEffect(() => {
getTotalNotes();
}, [ids, type, getTotalNotes]);
const getTotalNotes = React.useCallback(
(ids: string[]) => {
if (!ids || !ids.length || !type) return;
db.relations
.from({ type: type, ids: ids as string[] }, ["note"])
.get()
.then((relations) => {
const totalNotesById: any = {};
for (const id of ids) {
totalNotesById[id] = relations.filter(
(relation) => relation.fromId === id && relation.toType === "note"
)?.length;
}
setTotalNotesById(totalNotesById);
});
console.log("useTotalNotes.getTotalNotes");
},
[type]
);
return {
totalNotes: (id: string) => {

View File

@@ -24,40 +24,47 @@ import { eGroupOptionsUpdated, eOnNotebookUpdated } from "../utils/events";
import { useDBItem, useTotalNotes } from "./use-db-item";
export const useNotebook = (
id?: string,
items?: VirtualizedGrouping<Notebook>
id?: string | number,
items?: VirtualizedGrouping<Notebook>,
nestedNotebooks?: boolean
) => {
const [item, refresh] = useDBItem(id, "notebook", items);
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions("notebooks")
);
const [notebooks, setNotebooks] = useState<VirtualizedGrouping<Notebook>>();
const { totalNotes: nestedNotebookNotesCount } = useTotalNotes(
notebooks?.ids as string[],
"notebook"
);
const { totalNotes: nestedNotebookNotesCount, getTotalNotes } =
useTotalNotes("notebook");
const onRequestUpdate = React.useCallback(() => {
if (!id) return;
console.log("useNotebook.onRequestUpdate", id, Date.now());
db.relations
.from(
{
type: "notebook",
id: id
},
"notebook"
)
.selector.sorted(db.settings.getGroupOptions("notebooks"))
if (!item?.id) return;
console.log("useNotebook.onRequestUpdate", item?.id, Date.now());
const selector = db.relations.from(
{
type: "notebook",
id: item.id
},
"notebook"
).selector;
selector.ids().then((notebookIds) => {
getTotalNotes(notebookIds);
});
selector
.sorted(db.settings.getGroupOptions("notebooks"))
.then((notebooks) => {
setNotebooks(notebooks);
});
}, [id]);
}, [getTotalNotes, item?.id]);
useEffect(() => {
console.log("useNotebook.useEffect.onRequestUpdate");
onRequestUpdate();
}, [onRequestUpdate]);
if (nestedNotebooks) {
console.log("useNotebook.useEffect.onRequestUpdate");
onRequestUpdate();
}
}, [item?.id, onRequestUpdate, nestedNotebooks]);
const onUpdate = useCallback(
(type: string) => {
@@ -73,7 +80,9 @@ export const useNotebook = (
const onNotebookUpdate = (id?: string) => {
if (typeof id === "string" && id !== id) return;
setImmediate(() => {
onRequestUpdate();
if (nestedNotebooks) {
onRequestUpdate();
}
refresh();
});
};
@@ -84,7 +93,7 @@ export const useNotebook = (
eUnSubscribeEvent(eGroupOptionsUpdated, onUpdate);
eUnSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate);
};
}, [onUpdate, onRequestUpdate, id, refresh]);
}, [onUpdate, onRequestUpdate, id, refresh, nestedNotebooks]);
return {
notebook: item,

View File

@@ -33,11 +33,13 @@ export const useNoteStore = create<NoteStore>((set) => ({
notes: undefined,
loading: true,
setLoading: (loading) => set({ loading: loading }),
setNotes: () => {
db.notes.all.grouped(db.settings.getGroupOptions("home")).then((notes) => {
set({
notes: notes
});
setNotes: async () => {
const notes = await db.notes.all.grouped(
db.settings.getGroupOptions("home")
);
await notes.item(0);
set({
notes: notes
});
},
clearNotes: () => set({ notes: undefined })

View File

@@ -17,20 +17,24 @@ 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 { Item, Note, Notebook, VirtualizedGrouping } from "@notesnook/core";
import {
Item,
Note,
Notebook,
Tag,
VirtualizedGrouping
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import {
FlatList,
Platform,
StatusBar,
Text,
TextInput,
TouchableOpacity,
View,
useWindowDimensions
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import create from "zustand";
import { db } from "../app/common/database";
@@ -121,23 +125,29 @@ const NotebookItem = ({
items,
level = 0
}: {
id: string;
id: string | number;
mode: SearchMode;
close?: () => void;
level?: number;
items?: VirtualizedGrouping<Notebook>;
}) => {
const isExpanded = useNotebookExpandedStore((state) => state.expanded[id]);
const { nestedNotebooks, notebook } = useNotebook(id, items);
const isExpanded = useNotebookExpandedStore((state) =>
!notebook ? false : state.expanded[notebook.id]
);
const { colors } = useThemeColors();
const isSelected = useShareStore(
(state) =>
state.selectedNotebooks.findIndex((selectedId) => id === selectedId) > -1
const isSelected = useShareStore((state) =>
!notebook
? false
: state.selectedNotebooks.findIndex(
(selectedId) => notebook?.id === selectedId
) > -1
);
const set = SearchSetters[mode];
const onSelectItem = async () => {
set(id);
if (!notebook) return;
set(notebook.id);
};
return !notebook ? (
@@ -172,7 +182,8 @@ const NotebookItem = ({
alignItems: "center"
}}
onPress={() => {
useNotebookExpandedStore.getState().setExpanded(id);
if (!notebook) return;
useNotebookExpandedStore.getState().setExpanded(notebook.id);
}}
activeOpacity={1}
>
@@ -230,10 +241,10 @@ const NotebookItem = ({
marginTop: 5
}}
>
{(nestedNotebooks.ids as string[]).map((id) => (
{nestedNotebooks.ids.map((item, index) => (
<NotebookItem
key={id}
id={id}
key={notebook?.id + index}
id={index}
mode={mode}
close={close}
level={level + 1}
@@ -251,7 +262,7 @@ const ListItem = ({
close,
items
}: {
id: string;
id: string | number;
mode: SearchMode;
close?: () => void;
items?: VirtualizedGrouping<Item>;
@@ -259,21 +270,23 @@ const ListItem = ({
const [item] = useDBItem(
id,
mode === "appendNote" ? "note" : "tag",
items as any
items as VirtualizedGrouping<Note | Tag>
);
const { colors } = useThemeColors();
const isSelected = useShareStore((state) =>
mode === "appendNote" ? false : state.selectedTags.indexOf(id as never) > -1
mode === "appendNote" || !item
? false
: state.selectedTags.indexOf(item?.id as never) > -1
);
const set = SearchSetters[mode];
const onSelectItem = async () => {
if ((item as Note)?.locked) {
if ((item as Note)?.locked || !item) {
return;
}
set(id);
set(item.id);
};
return !item ? null : (
@@ -356,12 +369,6 @@ export const Search = ({
.then((exists) => setQueryExists(!!exists));
};
const insets =
Platform.OS === "android"
? { top: StatusBar.currentHeight }
: // eslint-disable-next-line react-hooks/rules-of-hooks
useSafeAreaInsets();
const get = SearchGetters[mode];
const lookup = SearchLookup[mode];
@@ -389,16 +396,16 @@ export const Search = ({
}, [get]);
const renderItem = React.useCallback(
({ item }: { item: string }) =>
({ index }: { item: string | number; index: number }) =>
mode === "selectNotebooks" ? (
<NotebookItem
id={item}
id={index}
mode={mode}
close={close}
items={items as any}
items={items as VirtualizedGrouping<Notebook>}
/>
) : (
<ListItem id={item} mode={mode} close={close} items={items} />
<ListItem id={index} mode={mode} close={close} items={items} />
),
[close, mode, items]
);
@@ -502,7 +509,7 @@ export const Search = ({
) : null}
<FlatList
data={items?.ids as string[]}
data={items?.ids}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
renderItem={renderItem}