mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 06:59:31 +01:00
global: move item resolution logic to @notesnook/common
This commit is contained in:
@@ -45,10 +45,7 @@ import { ReminderTime } from "../../ui/reminder-time";
|
|||||||
import { TimeSince } from "../../ui/time-since";
|
import { TimeSince } from "../../ui/time-since";
|
||||||
import Heading from "../../ui/typography/heading";
|
import Heading from "../../ui/typography/heading";
|
||||||
import Paragraph from "../../ui/typography/paragraph";
|
import Paragraph from "../../ui/typography/paragraph";
|
||||||
import {
|
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||||
NotebooksWithDateEdited,
|
|
||||||
TagsWithDateEdited
|
|
||||||
} from "../../../stores/resolve-items";
|
|
||||||
|
|
||||||
type NoteItemProps = {
|
type NoteItemProps = {
|
||||||
item: Note | BaseTrashItem<Note>;
|
item: Note | BaseTrashItem<Note>;
|
||||||
|
|||||||
@@ -36,10 +36,7 @@ import { tabBarRef } from "../../../utils/global-refs";
|
|||||||
|
|
||||||
import NotePreview from "../../note-history/preview";
|
import NotePreview from "../../note-history/preview";
|
||||||
import SelectionWrapper from "../selection-wrapper";
|
import SelectionWrapper from "../selection-wrapper";
|
||||||
import {
|
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||||
NotebooksWithDateEdited,
|
|
||||||
TagsWithDateEdited
|
|
||||||
} from "../../../stores/resolve-items";
|
|
||||||
|
|
||||||
export const openNote = async (
|
export const openNote = async (
|
||||||
item: Note,
|
item: Note,
|
||||||
|
|||||||
@@ -35,12 +35,6 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
|
|||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { db } from "../../common/database";
|
import { db } from "../../common/database";
|
||||||
import { eSendEvent } from "../../services/event-manager";
|
import { eSendEvent } from "../../services/event-manager";
|
||||||
import {
|
|
||||||
NotebooksWithDateEdited,
|
|
||||||
TagsWithDateEdited,
|
|
||||||
isNoteResolvedData,
|
|
||||||
resolveItems
|
|
||||||
} from "../../stores/resolve-items";
|
|
||||||
import { RouteName } from "../../stores/use-navigation-store";
|
import { RouteName } from "../../stores/use-navigation-store";
|
||||||
import { eOpenJumpToDialog } from "../../utils/events";
|
import { eOpenJumpToDialog } from "../../utils/events";
|
||||||
import { SectionHeader } from "../list-items/headers/section-header";
|
import { SectionHeader } from "../list-items/headers/section-header";
|
||||||
@@ -48,6 +42,12 @@ import { NoteWrapper } from "../list-items/note/wrapper";
|
|||||||
import { NotebookWrapper } from "../list-items/notebook/wrapper";
|
import { NotebookWrapper } from "../list-items/notebook/wrapper";
|
||||||
import ReminderItem from "../list-items/reminder";
|
import ReminderItem from "../list-items/reminder";
|
||||||
import TagItem from "../list-items/tag";
|
import TagItem from "../list-items/tag";
|
||||||
|
import {
|
||||||
|
NotebooksWithDateEdited,
|
||||||
|
TagsWithDateEdited,
|
||||||
|
isNoteResolvedData,
|
||||||
|
resolveItems
|
||||||
|
} from "@notesnook/common";
|
||||||
|
|
||||||
type ListItemWrapperProps<TItem = Item> = {
|
type ListItemWrapperProps<TItem = Item> = {
|
||||||
group?: GroupingKey;
|
group?: GroupingKey;
|
||||||
@@ -92,7 +92,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
|||||||
notebooks.current = data.notebooks;
|
notebooks.current = data.notebooks;
|
||||||
reminder.current = data.reminder;
|
reminder.current = data.reminder;
|
||||||
color.current = data.color;
|
color.current = data.color;
|
||||||
attachmentsCount.current = data.attachmentsCount || 0;
|
attachmentsCount.current = data.attachments?.total || 0;
|
||||||
} else if (
|
} else if (
|
||||||
resolvedItem.item.type === "notebook" &&
|
resolvedItem.item.type === "notebook" &&
|
||||||
typeof data === "number"
|
typeof data === "number"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import { View } from "react-native";
|
|||||||
import { SIZE } from "../../utils/size";
|
import { SIZE } from "../../utils/size";
|
||||||
import { IconButton } from "../../components/ui/icon-button";
|
import { IconButton } from "../../components/ui/icon-button";
|
||||||
import { PressableButton } from "../../components/ui/pressable";
|
import { PressableButton } from "../../components/ui/pressable";
|
||||||
import { resolveItems } from "../../stores/resolve-items";
|
import { resolveItems } from "@notesnook/common";
|
||||||
|
|
||||||
const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||||
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
|
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
eUnSubscribeEvent
|
eUnSubscribeEvent
|
||||||
} from "../../services/event-manager";
|
} from "../../services/event-manager";
|
||||||
import Navigation, { NavigationProps } from "../../services/navigation";
|
import Navigation, { NavigationProps } from "../../services/navigation";
|
||||||
import { resolveItems } from "../../stores/resolve-items";
|
import { resolveItems } from "@notesnook/common";
|
||||||
import useNavigationStore, {
|
import useNavigationStore, {
|
||||||
HeaderRightButton,
|
HeaderRightButton,
|
||||||
NotesScreenParams,
|
NotesScreenParams,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import { Item, VirtualizedGrouping } from "@notesnook/core";
|
import { Item, VirtualizedGrouping } from "@notesnook/core";
|
||||||
import create, { State, StoreApi, UseBoundStore } from "zustand";
|
import create, { State, StoreApi, UseBoundStore } from "zustand";
|
||||||
import { resolveItems } from "./resolve-items";
|
import { resolveItems } from "@notesnook/common";
|
||||||
import { useSettingStore } from "./use-setting-store";
|
import { useSettingStore } from "./use-setting-store";
|
||||||
|
|
||||||
export interface DBCollectionStore<Type extends Item> extends State {
|
export interface DBCollectionStore<Type extends Item> extends State {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { useMenuTrigger } from "../../hooks/use-menu";
|
|||||||
import { MenuItem } from "@notesnook/ui";
|
import { MenuItem } from "@notesnook/ui";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { Tag } from "@notesnook/core";
|
import { Tag } from "@notesnook/core";
|
||||||
import usePromise from "../../hooks/use-promise";
|
import { usePromise } from "@notesnook/common";
|
||||||
|
|
||||||
type HeaderProps = { readonly: boolean };
|
type HeaderProps = { readonly: boolean };
|
||||||
function Header(props: HeaderProps) {
|
function Header(props: HeaderProps) {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export function FilteredList<T>(props: FilteredListProps<T>) {
|
|||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
data-test-id={"filter-input"}
|
data-test-id={"filter-input"}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
sx={{ m: 0 }}
|
||||||
placeholder={
|
placeholder={
|
||||||
items.length <= 0 ? placeholders.empty : placeholders.filter
|
items.length <= 0 ? placeholders.empty : placeholders.filter
|
||||||
}
|
}
|
||||||
@@ -84,7 +85,9 @@ export function FilteredList<T>(props: FilteredListProps<T>) {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
py: 2
|
py: 2,
|
||||||
|
width: "100%",
|
||||||
|
mt: 1
|
||||||
}}
|
}}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await _createNewItem(query);
|
await _createNewItem(query);
|
||||||
|
|||||||
@@ -31,13 +31,7 @@ import { ListLoader } from "../loaders/list-loader";
|
|||||||
import ScrollContainer from "../scroll-container";
|
import ScrollContainer from "../scroll-container";
|
||||||
import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
|
import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
|
||||||
import { Context } from "./types";
|
import { Context } from "./types";
|
||||||
import {
|
import { VirtualizedGrouping, GroupingKey, Item } from "@notesnook/core";
|
||||||
VirtualizedGrouping,
|
|
||||||
GroupingKey,
|
|
||||||
Item,
|
|
||||||
isGroupHeader
|
|
||||||
} from "@notesnook/core";
|
|
||||||
import { useResolvedItem } from "./resolved-item";
|
|
||||||
import {
|
import {
|
||||||
ItemProps,
|
ItemProps,
|
||||||
ScrollerProps,
|
ScrollerProps,
|
||||||
@@ -45,6 +39,7 @@ import {
|
|||||||
VirtuosoHandle
|
VirtuosoHandle
|
||||||
} from "react-virtuoso";
|
} from "react-virtuoso";
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
import { useResolvedItem } from "@notesnook/common";
|
||||||
|
|
||||||
export const CustomScrollbarsVirtualList = forwardRef<
|
export const CustomScrollbarsVirtualList = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import Reminder from "../reminder";
|
|||||||
import { Context } from "./types";
|
import { Context } from "./types";
|
||||||
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
|
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
|
||||||
import { GroupingKey, Item } from "@notesnook/core";
|
import { GroupingKey, Item } from "@notesnook/core";
|
||||||
|
import { isNoteResolvedData } from "@notesnook/common";
|
||||||
import { Attachment } from "../attachment";
|
import { Attachment } from "../attachment";
|
||||||
import { isNoteResolvedData } from "./resolved-item";
|
|
||||||
|
|
||||||
const SINGLE_LINE_HEIGHT = 1.4;
|
const SINGLE_LINE_HEIGHT = 1.4;
|
||||||
const DEFAULT_LINE_HEIGHT =
|
const DEFAULT_LINE_HEIGHT =
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
/*
|
|
||||||
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 {
|
|
||||||
Color,
|
|
||||||
Item,
|
|
||||||
ItemMap,
|
|
||||||
ItemType,
|
|
||||||
Reminder,
|
|
||||||
VirtualizedGrouping
|
|
||||||
} from "@notesnook/core";
|
|
||||||
import usePromise from "../../hooks/use-promise";
|
|
||||||
import {
|
|
||||||
NotebooksWithDateEdited,
|
|
||||||
TagsWithDateEdited,
|
|
||||||
WithDateEdited
|
|
||||||
} from "./types";
|
|
||||||
import { db } from "../../common/db";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
type ResolvedItemProps<TItemType extends ItemType> = {
|
|
||||||
type: TItemType;
|
|
||||||
items: VirtualizedGrouping<ItemMap[TItemType]>;
|
|
||||||
index: number;
|
|
||||||
children: (item: {
|
|
||||||
item: ItemMap[TItemType];
|
|
||||||
data: unknown;
|
|
||||||
}) => React.ReactNode;
|
|
||||||
};
|
|
||||||
export function ResolvedItem<TItemType extends ItemType>(
|
|
||||||
props: ResolvedItemProps<TItemType>
|
|
||||||
) {
|
|
||||||
const { index, items, children, type } = props;
|
|
||||||
const result = usePromise(
|
|
||||||
() => items.item(index, resolveItems),
|
|
||||||
[index, items]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.status === "rejected" || !result.value) return null;
|
|
||||||
|
|
||||||
if (result.value.item.type !== type) return null;
|
|
||||||
return <>{children(result.value)}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useResolvedItem(
|
|
||||||
props: Omit<ResolvedItemProps<ItemType>, "children" | "type">
|
|
||||||
) {
|
|
||||||
const { index, items } = props;
|
|
||||||
const result = usePromise(
|
|
||||||
() => items.item(index, resolveItems),
|
|
||||||
[index, items]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.status === "rejected" || !result.value) return null;
|
|
||||||
return result.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function withDateEdited<
|
|
||||||
T extends { dateEdited: number } | { dateModified: number }
|
|
||||||
>(items: T[]): WithDateEdited<T> {
|
|
||||||
let latestDateEdited = 0;
|
|
||||||
items.forEach((item) => {
|
|
||||||
const date = "dateEdited" in item ? item.dateEdited : item.dateModified;
|
|
||||||
if (latestDateEdited < date) latestDateEdited = date;
|
|
||||||
});
|
|
||||||
return { dateEdited: latestDateEdited, items };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveItems(ids: string[], items: Item[]) {
|
|
||||||
if (!ids.length || !items.length) return [];
|
|
||||||
|
|
||||||
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 [];
|
|
||||||
}
|
|
||||||
|
|
||||||
type NoteResolvedData = {
|
|
||||||
notebooks?: NotebooksWithDateEdited;
|
|
||||||
reminder?: Reminder;
|
|
||||||
color?: Color;
|
|
||||||
tags?: TagsWithDateEdited;
|
|
||||||
};
|
|
||||||
async function resolveNotes(ids: string[]) {
|
|
||||||
console.time("relations");
|
|
||||||
const relations = [
|
|
||||||
...(await db.relations
|
|
||||||
.to({ type: "note", ids }, ["notebook", "tag", "color"])
|
|
||||||
.get()),
|
|
||||||
...(await db.relations.from({ type: "note", ids }, "reminder").get())
|
|
||||||
];
|
|
||||||
console.timeEnd("relations");
|
|
||||||
const relationIds: {
|
|
||||||
notebooks: Set<string>;
|
|
||||||
colors: Set<string>;
|
|
||||||
tags: Set<string>;
|
|
||||||
reminders: Set<string>;
|
|
||||||
} = {
|
|
||||||
colors: new Set(),
|
|
||||||
notebooks: new Set(),
|
|
||||||
tags: new Set(),
|
|
||||||
reminders: new Set()
|
|
||||||
};
|
|
||||||
|
|
||||||
const grouped: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
notebooks: string[];
|
|
||||||
color?: string;
|
|
||||||
tags: string[];
|
|
||||||
reminder?: string;
|
|
||||||
}
|
|
||||||
> = {};
|
|
||||||
for (const relation of relations) {
|
|
||||||
const noteId =
|
|
||||||
relation.toType === "reminder" ? relation.fromId : relation.toId;
|
|
||||||
const data = grouped[noteId] || {
|
|
||||||
notebooks: [],
|
|
||||||
tags: []
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
} else if (relation.fromType === "tag" && data.tags.length < 3) {
|
|
||||||
data.tags.push(relation.fromId);
|
|
||||||
relationIds.tags.add(relation.fromId);
|
|
||||||
} else if (relation.fromType === "color" && !data.color) {
|
|
||||||
data.color = relation.fromId;
|
|
||||||
relationIds.colors.add(relation.fromId);
|
|
||||||
}
|
|
||||||
grouped[noteId] = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.time("resolve");
|
|
||||||
const resolved = {
|
|
||||||
notebooks: await db.notebooks.all.records(
|
|
||||||
Array.from(relationIds.notebooks)
|
|
||||||
),
|
|
||||||
tags: await db.tags.all.records(Array.from(relationIds.tags)),
|
|
||||||
colors: await db.colors.all.records(Array.from(relationIds.colors)),
|
|
||||||
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
|
|
||||||
};
|
|
||||||
console.timeEnd("resolve");
|
|
||||||
|
|
||||||
const data: NoteResolvedData[] = [];
|
|
||||||
for (const noteId of ids) {
|
|
||||||
const group = grouped[noteId];
|
|
||||||
if (!group) {
|
|
||||||
data.push({});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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]).filter(Boolean)
|
|
||||||
),
|
|
||||||
notebooks: withDateEdited(
|
|
||||||
group.notebooks.map((id) => resolved.notebooks[id]).filter(Boolean)
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isNoteResolvedData(data: unknown): data is NoteResolvedData {
|
|
||||||
return (
|
|
||||||
typeof data === "object" &&
|
|
||||||
!!data &&
|
|
||||||
"notebooks" in data &&
|
|
||||||
"reminder" in data &&
|
|
||||||
"color" in data &&
|
|
||||||
"tags" in data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -76,9 +76,12 @@ import {
|
|||||||
isReminderActive,
|
isReminderActive,
|
||||||
isReminderToday
|
isReminderToday
|
||||||
} from "@notesnook/core/dist/collections/reminders";
|
} from "@notesnook/core/dist/collections/reminders";
|
||||||
import { getFormattedReminderTime, pluralize } from "@notesnook/common";
|
|
||||||
import {
|
import {
|
||||||
Reminder as ReminderType,
|
NoteResolvedData,
|
||||||
|
getFormattedReminderTime,
|
||||||
|
pluralize
|
||||||
|
} from "@notesnook/common";
|
||||||
|
import {
|
||||||
Color,
|
Color,
|
||||||
Note,
|
Note,
|
||||||
Notebook as NotebookItem,
|
Notebook as NotebookItem,
|
||||||
@@ -86,22 +89,14 @@ import {
|
|||||||
DefaultColors
|
DefaultColors
|
||||||
} from "@notesnook/core";
|
} from "@notesnook/core";
|
||||||
import { MenuItem } from "@notesnook/ui";
|
import { MenuItem } from "@notesnook/ui";
|
||||||
import {
|
import { Context } from "../list-container/types";
|
||||||
Context,
|
|
||||||
NotebooksWithDateEdited,
|
|
||||||
TagsWithDateEdited
|
|
||||||
} from "../list-container/types";
|
|
||||||
import { SchemeColors } from "@notesnook/theme";
|
import { SchemeColors } from "@notesnook/theme";
|
||||||
import FileSaver from "file-saver";
|
import FileSaver from "file-saver";
|
||||||
|
|
||||||
type NoteProps = {
|
type NoteProps = NoteResolvedData & {
|
||||||
tags?: TagsWithDateEdited;
|
|
||||||
color?: Color;
|
|
||||||
notebooks?: NotebooksWithDateEdited;
|
|
||||||
item: Note;
|
item: Note;
|
||||||
context?: Context;
|
context?: Context;
|
||||||
date: number;
|
date: number;
|
||||||
reminder?: ReminderType;
|
|
||||||
simplified?: boolean;
|
simplified?: boolean;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
};
|
};
|
||||||
@@ -111,6 +106,7 @@ function Note(props: NoteProps) {
|
|||||||
tags,
|
tags,
|
||||||
color,
|
color,
|
||||||
notebooks,
|
notebooks,
|
||||||
|
attachments,
|
||||||
item,
|
item,
|
||||||
date,
|
date,
|
||||||
reminder,
|
reminder,
|
||||||
@@ -121,17 +117,6 @@ function Note(props: NoteProps) {
|
|||||||
const note = item;
|
const note = item;
|
||||||
|
|
||||||
const isOpened = useStore((store) => store.selectedNote === note.id);
|
const isOpened = useStore((store) => store.selectedNote === note.id);
|
||||||
const attachments = [];
|
|
||||||
|
|
||||||
// useAttachmentStore((store) =>
|
|
||||||
// store.attachments.filter((a) => a.noteIds.includes(note.id))
|
|
||||||
// );
|
|
||||||
const failed = [];
|
|
||||||
|
|
||||||
// useMemo(
|
|
||||||
// () => attachments.filter((a) => a.failed),
|
|
||||||
// [attachments]
|
|
||||||
// );
|
|
||||||
const primary: SchemeColors = color ? color.colorCode : "accent-selected";
|
const primary: SchemeColors = color ? color.colorCode : "accent-selected";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -226,21 +211,21 @@ function Note(props: NoteProps) {
|
|||||||
datetime={date}
|
datetime={date}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{attachments.length > 0 && (
|
{attachments?.total ? (
|
||||||
<Flex sx={{ alignItems: "center", justifyContent: "center" }}>
|
<Flex sx={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
<Attachment size={13} />
|
<Attachment size={13} />
|
||||||
<Text variant="subBody" ml={"2px"}>
|
<Text variant="subBody" ml={"2px"}>
|
||||||
{attachments.length}
|
{attachments.total}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{failed.length > 0 && (
|
{attachments?.failed ? (
|
||||||
<Flex title={`Errors in ${failed.length} attachments.`}>
|
<Flex title={`Errors in ${attachments.failed} attachments.`}>
|
||||||
<AttachmentError size={13} color="var(--icon-error)" />
|
<AttachmentError size={13} color="var(--icon-error)" />
|
||||||
<Text ml={"2px"}>{failed.length}</Text>
|
<Text ml={"2px"}>{attachments.failed}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{note.pinned && !props.context && (
|
{note.pinned && !props.context && (
|
||||||
<Pin size={13} color={primary} />
|
<Pin size={13} color={primary} />
|
||||||
@@ -289,7 +274,8 @@ export default React.memo(Note, function (prevProps, nextProps) {
|
|||||||
prevItem.dateModified === nextItem.dateModified &&
|
prevItem.dateModified === nextItem.dateModified &&
|
||||||
prevProps.notebooks?.dateEdited === nextProps.notebooks?.dateEdited &&
|
prevProps.notebooks?.dateEdited === nextProps.notebooks?.dateEdited &&
|
||||||
prevProps.tags?.dateEdited === nextProps.tags?.dateEdited &&
|
prevProps.tags?.dateEdited === nextProps.tags?.dateEdited &&
|
||||||
prevProps.reminder?.dateModified === nextProps.reminder?.dateModified
|
prevProps.reminder?.dateModified === nextProps.reminder?.dateModified &&
|
||||||
|
prevProps.attachments === nextProps.attachments
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,13 +36,11 @@ import { store as noteStore } from "../../stores/note-store";
|
|||||||
import { AnimatedFlex } from "../animated";
|
import { AnimatedFlex } from "../animated";
|
||||||
import Toggle from "./toggle";
|
import Toggle from "./toggle";
|
||||||
import ScrollContainer from "../scroll-container";
|
import ScrollContainer from "../scroll-container";
|
||||||
import { getFormattedDate } from "@notesnook/common";
|
import { ResolvedItem, getFormattedDate, usePromise } from "@notesnook/common";
|
||||||
import { ScopedThemeProvider } from "../theme-provider";
|
import { ScopedThemeProvider } from "../theme-provider";
|
||||||
import { PreviewSession } from "../editor/types";
|
import { PreviewSession } from "../editor/types";
|
||||||
import usePromise from "../../hooks/use-promise";
|
|
||||||
import { ListItemWrapper } from "../list-container/list-profiles";
|
import { ListItemWrapper } from "../list-container/list-profiles";
|
||||||
import { VirtualizedList } from "../virtualized-list";
|
import { VirtualizedList } from "../virtualized-list";
|
||||||
import { ResolvedItem } from "../list-container/resolved-item";
|
|
||||||
import { SessionItem } from "../session-item";
|
import { SessionItem } from "../session-item";
|
||||||
import { COLORS } from "../../common/constants";
|
import { COLORS } from "../../common/constants";
|
||||||
import { DefaultColors } from "@notesnook/core";
|
import { DefaultColors } from "@notesnook/core";
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ import { ArrowLeft, Menu, Search, Plus, Close } from "../icons";
|
|||||||
import { useStore } from "../../stores/app-store";
|
import { useStore } from "../../stores/app-store";
|
||||||
import { useStore as useSearchStore } from "../../stores/search-store";
|
import { useStore as useSearchStore } from "../../stores/search-store";
|
||||||
import useMobile from "../../hooks/use-mobile";
|
import useMobile from "../../hooks/use-mobile";
|
||||||
import usePromise from "../../hooks/use-promise";
|
import { debounce, usePromise } from "@notesnook/common";
|
||||||
import { debounce } from "@notesnook/common";
|
|
||||||
import Field from "../field";
|
import Field from "../field";
|
||||||
|
|
||||||
export type RouteContainerButtons = {
|
export type RouteContainerButtons = {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { useStore as useAppStore } from "../../stores/app-store";
|
|||||||
import Field from "../field";
|
import Field from "../field";
|
||||||
import { showToast } from "../../utils/toast";
|
import { showToast } from "../../utils/toast";
|
||||||
import { ErrorText } from "../error-text";
|
import { ErrorText } from "../error-text";
|
||||||
import usePromise from "../../hooks/use-promise";
|
import { usePromise } from "@notesnook/common";
|
||||||
|
|
||||||
type UnlockProps = {
|
type UnlockProps = {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ import { store as editorStore } from "../stores/editor-store";
|
|||||||
import { Perform } from "../common/dialog-controller";
|
import { Perform } from "../common/dialog-controller";
|
||||||
import { FilteredList } from "../components/filtered-list";
|
import { FilteredList } from "../components/filtered-list";
|
||||||
import { ItemReference, Tag } from "@notesnook/core/dist/types";
|
import { ItemReference, Tag } from "@notesnook/core/dist/types";
|
||||||
import { ResolvedItem } from "../components/list-container/resolved-item";
|
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { VirtualizedGrouping } from "@notesnook/core";
|
import { VirtualizedGrouping } from "@notesnook/core";
|
||||||
|
import { ResolvedItem } from "@notesnook/common";
|
||||||
|
|
||||||
type SelectedReference = {
|
type SelectedReference = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -134,6 +134,8 @@ function AddTagsDialog(props: AddTagsDialogProps) {
|
|||||||
onCreateNewItem={async (title) => {
|
onCreateNewItem={async (title) => {
|
||||||
const tagId = await db.tags.add({ title });
|
const tagId = await db.tags.add({ title });
|
||||||
if (!tagId) return;
|
if (!tagId) return;
|
||||||
|
await useStore.getState().refresh();
|
||||||
|
setTags(useStore.getState().tags);
|
||||||
const { selected, setSelected } = useSelectionStore.getState();
|
const { selected, setSelected } = useSelectionStore.getState();
|
||||||
setSelected([...selected, { id: tagId, new: true, op: "add" }]);
|
setSelected([...selected, { id: tagId, new: true, op: "add" }]);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
Text
|
Text
|
||||||
} from "@theme-ui/components";
|
} from "@theme-ui/components";
|
||||||
import { store, useStore } from "../stores/attachment-store";
|
import { store, useStore } from "../stores/attachment-store";
|
||||||
import { formatBytes } from "@notesnook/common";
|
import { ResolvedItem, formatBytes, usePromise } from "@notesnook/common";
|
||||||
import Dialog from "../components/dialog";
|
import Dialog from "../components/dialog";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -57,13 +57,11 @@ import {
|
|||||||
VirtualizedGrouping
|
VirtualizedGrouping
|
||||||
} from "@notesnook/core";
|
} from "@notesnook/core";
|
||||||
import { Multiselect } from "../common/multi-select";
|
import { Multiselect } from "../common/multi-select";
|
||||||
import { ResolvedItem } from "../components/list-container/resolved-item";
|
|
||||||
import {
|
import {
|
||||||
VirtualizedTable,
|
VirtualizedTable,
|
||||||
VirtualizedTableRowProps
|
VirtualizedTableRowProps
|
||||||
} from "../components/virtualized-table";
|
} from "../components/virtualized-table";
|
||||||
import { FlexScrollContainer } from "../components/scroll-container";
|
import { FlexScrollContainer } from "../components/scroll-container";
|
||||||
import usePromise from "../hooks/use-promise";
|
|
||||||
|
|
||||||
type ToolbarAction = {
|
type ToolbarAction = {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ import {
|
|||||||
TreeEnvironmentRef
|
TreeEnvironmentRef
|
||||||
} from "react-complex-tree";
|
} from "react-complex-tree";
|
||||||
import { FlexScrollContainer } from "../components/scroll-container";
|
import { FlexScrollContainer } from "../components/scroll-container";
|
||||||
import { pluralize } from "@notesnook/common";
|
import { pluralize, usePromise } from "@notesnook/common";
|
||||||
import usePromise from "../hooks/use-promise";
|
|
||||||
|
|
||||||
type MoveDialogProps = { onClose: Perform; noteIds: string[] };
|
type MoveDialogProps = { onClose: Perform; noteIds: string[] };
|
||||||
type NotebookReference = {
|
type NotebookReference = {
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ import { Reminder } from "@notesnook/core/dist/types";
|
|||||||
import IconTag from "../components/icon-tag";
|
import IconTag from "../components/icon-tag";
|
||||||
import { Clock, Refresh } from "../components/icons";
|
import { Clock, Refresh } from "../components/icons";
|
||||||
import Note from "../components/note";
|
import Note from "../components/note";
|
||||||
import { getFormattedReminderTime } from "@notesnook/common";
|
import { getFormattedReminderTime, usePromise } from "@notesnook/common";
|
||||||
import usePromise from "../hooks/use-promise";
|
|
||||||
|
|
||||||
export type ReminderPreviewDialogProps = {
|
export type ReminderPreviewDialogProps = {
|
||||||
onClose: Perform;
|
onClose: Perform;
|
||||||
|
|||||||
20
packages/common/src/components/index.ts
Normal file
20
packages/common/src/components/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./resolved-item";
|
||||||
39
packages/common/src/components/resolved-item.tsx
Normal file
39
packages/common/src/components/resolved-item.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
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 { ItemMap, ItemType } from "@notesnook/core";
|
||||||
|
import {
|
||||||
|
ResolvedItemOptions,
|
||||||
|
useResolvedItem
|
||||||
|
} from "../hooks/use-resolved-item";
|
||||||
|
|
||||||
|
type ResolvedItemProps<TItemType extends ItemType> =
|
||||||
|
ResolvedItemOptions<TItemType> & {
|
||||||
|
children: (item: {
|
||||||
|
item: ItemMap[TItemType];
|
||||||
|
data: unknown;
|
||||||
|
}) => React.ReactNode;
|
||||||
|
};
|
||||||
|
export function ResolvedItem<TItemType extends ItemType>(
|
||||||
|
props: ResolvedItemProps<TItemType>
|
||||||
|
) {
|
||||||
|
const { children } = props;
|
||||||
|
const result = useResolvedItem(props);
|
||||||
|
return result ? <>{children(result)}</> : null;
|
||||||
|
}
|
||||||
@@ -18,3 +18,5 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./use-time-ago";
|
export * from "./use-time-ago";
|
||||||
|
export * from "./use-promise";
|
||||||
|
export * from "./use-resolved-item";
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export type PromiseFactoryFn<T> = (signal: AbortSignal) => T | Promise<T>;
|
|||||||
* @param factory Function that creates the promise.
|
* @param factory Function that creates the promise.
|
||||||
* @param deps If present, promise will be recreated if the values in the list change.
|
* @param deps If present, promise will be recreated if the values in the list change.
|
||||||
*/
|
*/
|
||||||
export default function usePromise<T>(
|
export function usePromise<T>(
|
||||||
factory: PromiseFactoryFn<T>,
|
factory: PromiseFactoryFn<T>,
|
||||||
deps: DependencyList = []
|
deps: DependencyList = []
|
||||||
): PromiseResult<T> {
|
): PromiseResult<T> {
|
||||||
59
packages/common/src/hooks/use-resolved-item.ts
Normal file
59
packages/common/src/hooks/use-resolved-item.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
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 { ItemMap, ItemType, VirtualizedGrouping } from "@notesnook/core";
|
||||||
|
import { usePromise } from "./use-promise";
|
||||||
|
import { resolveItems } from "../utils/resolve-items";
|
||||||
|
|
||||||
|
export type ResolvedItemOptions<TItemType extends ItemType> = {
|
||||||
|
type?: TItemType;
|
||||||
|
items: VirtualizedGrouping<ItemMap[TItemType]>;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches & resolves the item from VirtualizedGrouping
|
||||||
|
*/
|
||||||
|
export function useResolvedItem<TItemType extends ItemType>(
|
||||||
|
options: ResolvedItemOptions<TItemType>
|
||||||
|
) {
|
||||||
|
const { index, items, type } = options;
|
||||||
|
const result = usePromise(
|
||||||
|
() => items.item(index, resolveItems),
|
||||||
|
[index, items]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.status === "rejected" || !result.value) return null;
|
||||||
|
if (type && result.value.item.type !== type) return null;
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches but does not resolve the item from VirtualizedGrouping
|
||||||
|
*/
|
||||||
|
export function useUnresolvedItem<TItemType extends ItemType>(
|
||||||
|
options: ResolvedItemOptions<TItemType>
|
||||||
|
) {
|
||||||
|
const { index, items, type } = options;
|
||||||
|
const result = usePromise(() => items.item(index), [index, items]);
|
||||||
|
|
||||||
|
if (result.status === "rejected" || !result.value) return null;
|
||||||
|
if (type && result.value.item.type !== type) return null;
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
@@ -20,3 +20,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
export * from "./database";
|
export * from "./database";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./hooks";
|
export * from "./hooks";
|
||||||
|
export * from "./components";
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ export * from "./time";
|
|||||||
export * from "./debounce";
|
export * from "./debounce";
|
||||||
export * from "./random";
|
export * from "./random";
|
||||||
export * from "./string";
|
export * from "./string";
|
||||||
|
export * from "./resolve-items";
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Color, Item, Notebook, Reminder, Tag } from "@notesnook/core";
|
|
||||||
import { db } from "../common/database";
|
|
||||||
|
|
||||||
export type WithDateEdited<T> = { items: T[]; dateEdited: number };
|
import { Color, Item, Reminder, Notebook, Tag } from "@notesnook/core";
|
||||||
|
import { database as db } from "../database";
|
||||||
|
|
||||||
|
type WithDateEdited<T> = { items: T[]; dateEdited: number };
|
||||||
export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
|
export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
|
||||||
export type TagsWithDateEdited = WithDateEdited<Tag>;
|
export type TagsWithDateEdited = WithDateEdited<Tag>;
|
||||||
|
|
||||||
@@ -49,22 +50,25 @@ export async function resolveItems(ids: string[], items: Item[]) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
type NoteResolvedData = {
|
export type NoteResolvedData = {
|
||||||
notebooks?: NotebooksWithDateEdited;
|
notebooks?: NotebooksWithDateEdited;
|
||||||
reminder?: Reminder;
|
reminder?: Reminder;
|
||||||
color?: Color;
|
color?: Color;
|
||||||
tags?: TagsWithDateEdited;
|
tags?: TagsWithDateEdited;
|
||||||
attachmentsCount?: number;
|
attachments?: {
|
||||||
|
failed: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function resolveNotes(ids: string[]) {
|
async function resolveNotes(ids: string[]) {
|
||||||
console.time("relations");
|
|
||||||
const relations = [
|
const relations = [
|
||||||
...(await db.relations
|
...(await db.relations
|
||||||
.to({ type: "note", ids }, ["notebook", "tag", "color"])
|
.to({ type: "note", ids }, ["notebook", "tag", "color"])
|
||||||
.get()),
|
.get()),
|
||||||
...(await db.relations.from({ type: "note", ids }, "reminder").get())
|
...(await db.relations.from({ type: "note", ids }, "reminder").get())
|
||||||
];
|
];
|
||||||
console.timeEnd("relations");
|
|
||||||
const relationIds: {
|
const relationIds: {
|
||||||
notebooks: Set<string>;
|
notebooks: Set<string>;
|
||||||
colors: Set<string>;
|
colors: Set<string>;
|
||||||
@@ -86,6 +90,7 @@ async function resolveNotes(ids: string[]) {
|
|||||||
reminder?: string;
|
reminder?: string;
|
||||||
}
|
}
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
for (const relation of relations) {
|
for (const relation of relations) {
|
||||||
const noteId =
|
const noteId =
|
||||||
relation.toType === "reminder" ? relation.fromId : relation.toId;
|
relation.toType === "reminder" ? relation.fromId : relation.toId;
|
||||||
@@ -110,7 +115,6 @@ async function resolveNotes(ids: string[]) {
|
|||||||
grouped[noteId] = data;
|
grouped[noteId] = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.time("resolve");
|
|
||||||
const resolved = {
|
const resolved = {
|
||||||
notebooks: await db.notebooks.all.records(
|
notebooks: await db.notebooks.all.records(
|
||||||
Array.from(relationIds.notebooks)
|
Array.from(relationIds.notebooks)
|
||||||
@@ -119,7 +123,6 @@ async function resolveNotes(ids: string[]) {
|
|||||||
colors: await db.colors.all.records(Array.from(relationIds.colors)),
|
colors: await db.colors.all.records(Array.from(relationIds.colors)),
|
||||||
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
|
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
|
||||||
};
|
};
|
||||||
console.timeEnd("resolve");
|
|
||||||
|
|
||||||
const data: NoteResolvedData[] = [];
|
const data: NoteResolvedData[] = [];
|
||||||
for (const noteId of ids) {
|
for (const noteId of ids) {
|
||||||
@@ -129,6 +132,7 @@ async function resolveNotes(ids: string[]) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attachments = db.attachments?.ofNote(noteId, "all");
|
||||||
data.push({
|
data.push({
|
||||||
color: group.color ? resolved.colors[group.color] : undefined,
|
color: group.color ? resolved.colors[group.color] : undefined,
|
||||||
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
|
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
|
||||||
@@ -138,11 +142,14 @@ async function resolveNotes(ids: string[]) {
|
|||||||
notebooks: withDateEdited(
|
notebooks: withDateEdited(
|
||||||
group.notebooks.map((id) => resolved.notebooks[id]).filter(Boolean)
|
group.notebooks.map((id) => resolved.notebooks[id]).filter(Boolean)
|
||||||
),
|
),
|
||||||
attachmentsCount:
|
attachments: {
|
||||||
(await db.attachments?.ofNote(noteId, "all").ids())?.length || 0
|
total: await attachments.count(),
|
||||||
|
failed: await attachments
|
||||||
|
.where((eb) => eb("attachments.failed", "is not", eb.lit(null)))
|
||||||
|
.count()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user