mobile: migrate to sqlite

This commit is contained in:
Ammar Ahmed
2023-11-16 08:54:37 +05:00
committed by Abdullah Atta
parent 2168609577
commit 5bfad0149b
110 changed files with 64611 additions and 11914 deletions

View File

@@ -50,6 +50,7 @@ const App = () => {
if (appLockMode && appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {

View File

@@ -23,9 +23,10 @@ import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import filesystem from "../filesystem";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
database.host(
__DEV__
@@ -57,6 +58,21 @@ database.setup({
compressor: {
compress: Gzip.deflate,
decompress: Gzip.inflate
},
batchSize: 500,
sqliteOptions: {
dialect: {
createDriver: () =>
new RNSqliteDriver({ async: true, dbName: "test.db" }),
createAdapter: () => new SqliteAdapter(),
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler()
}
// journalMode: "MEMORY",
// synchronous: "normal",
// pageSize: 8192,
// cacheSize: -16000,
// lockingMode: "exclusive"
}
});

View File

@@ -16,14 +16,14 @@ 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 { initalize } from "@notesnook/core/dist/logger";
import { MMKVLoader } from "react-native-mmkv-storage";
import { initialize } from "@notesnook/core/dist/logger";
import { KV } from "./storage";
const LoggerStorage = new MMKVLoader()
.withInstanceID("notesnook_logs")
.initialize();
initalize(new KV(LoggerStorage));
initialize(new KV(LoggerStorage));
export { LoggerStorage };

View File

@@ -0,0 +1,123 @@
/*
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 type { DatabaseConnection, Driver, QueryResult } from "kysely";
import { CompiledQuery } from "kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
type Config = { dbName: string; async: boolean; location: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;
private connectionMutex = new ConnectionMutex();
private db: QuickSQLiteConnection;
constructor(private readonly config: Config) {
this.db = open({
name: config.dbName
});
}
async init(): Promise<void> {
this.connection = new RNSqliteConnection(this.db);
}
async acquireConnection(): Promise<DatabaseConnection> {
// SQLite only has one single connection. We use a mutex here to wait
// until the single connection has been released.
await this.connectionMutex.lock();
return this.connection!;
}
async beginTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("begin"));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("commit"));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("rollback"));
}
async releaseConnection(): Promise<void> {
this.connectionMutex.unlock();
}
async destroy(): Promise<void> {
this.db.close();
}
}
class ConnectionMutex {
private promise?: Promise<void>;
private resolve?: () => void;
async lock(): Promise<void> {
while (this.promise) {
await this.promise;
}
this.promise = new Promise((resolve) => {
this.resolve = resolve;
});
}
unlock(): void {
const resolve = this.resolve;
this.promise = undefined;
this.resolve = undefined;
resolve?.();
}
}
class RNSqliteConnection implements DatabaseConnection {
constructor(private readonly db: QuickSQLiteConnection) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error("wasqlite driver doesn't support streaming");
}
async executeQuery<R>(
compiledQuery: CompiledQuery<unknown>
): Promise<QueryResult<R>> {
const { parameters, sql, query } = compiledQuery;
const mode =
query.kind === "SelectQueryNode"
? "query"
: query.kind === "RawNode"
? "raw"
: "exec";
const result = await this.db.executeAsync(sql, parameters as any[]);
console.log("SQLITE result:", result?.rows?._array);
if (mode === "query" || !result.insertId)
return {
rows: result.rows?._array || []
};
return {
insertId: BigInt(result.insertId),
numAffectedRows: BigInt(result.rowsAffected),
rows: mode === "raw" ? result.rows?._array || [] : []
};
}
}

View File

@@ -189,7 +189,7 @@ export default async function downloadAttachment(
) {
await createCacheDir();
let attachment = db.attachments.attachment(hash);
let attachment = await db.attachments.attachment(hash);
if (!attachment) {
console.log("attachment not found");
return;

View File

@@ -17,9 +17,13 @@ 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 { formatBytes } from "@notesnook/common";
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useState } from "react";
import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { ScrollView } from "react-native-gesture-handler";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
@@ -27,14 +31,17 @@ import downloadAttachment from "../../common/filesystem/download-attachment";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import picker from "../../screens/editor/tiptap/picker";
import {
ToastManager,
eSendEvent,
presentSheet,
ToastManager
presentSheet
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { useThemeColors } from "@notesnook/theme";
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
import {
eCloseAttachmentDialog,
eCloseSheet,
eDBItemUpdate
} from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
@@ -46,28 +53,37 @@ import { Notice } from "../ui/notice";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { formatBytes } from "@notesnook/common";
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
const Actions = ({
attachment,
close,
setAttachments,
fwdRef
}: {
attachment: Attachment;
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
close: () => void;
fwdRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const contextId = attachment.metadata.hash;
const [filename, setFilename] = useState(attachment.metadata.filename);
const contextId = attachment.hash;
const [filename, setFilename] = useState(attachment.filename);
const [currentProgress] = useAttachmentProgress(attachment);
const [failed, setFailed] = useState(attachment.failed);
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState({
name: null
});
const [failed, setFailed] = useState<string | undefined>(attachment.failed);
const [notes, setNotes] = useState<Note[]>([]);
const [loading, setLoading] = useState<{
name?: string;
}>({});
const actions = [
{
name: "Download",
onPress: async () => {
if (currentProgress) {
await db.fs().cancel(attachment.metadata.hash);
useAttachmentStore.getState().remove(attachment.metadata.hash);
await db.fs().cancel(attachment.hash);
useAttachmentStore.getState().remove(attachment.hash);
}
downloadAttachment(attachment.metadata.hash, false);
downloadAttachment(attachment.hash, false);
eSendEvent(eCloseSheet, contextId);
},
icon: "download"
@@ -85,9 +101,9 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
}
await picker.pick({
reupload: true,
hash: attachment.metadata.hash,
hash: attachment.hash,
context: contextId,
type: attachment.metadata.type
type: attachment.type
});
},
icon: "upload"
@@ -98,7 +114,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
setLoading({
name: "Run file check"
});
let res = await filesystem.checkAttachment(attachment.metadata.hash);
let res = await filesystem.checkAttachment(attachment.hash);
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
@@ -108,8 +124,9 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
context: "local"
});
} else {
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
setFailed(undefined);
db.attachments.markAsFailed(attachment.id);
eSendEvent(eDBItemUpdate, attachment.id);
ToastManager.show({
heading: "File check passed",
type: "success",
@@ -119,7 +136,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
setAttachments();
setLoading({
name: null
name: undefined
});
},
icon: "file-check"
@@ -128,19 +145,20 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
name: "Rename",
onPress: () => {
presentDialog({
context: contextId,
context: contextId as any,
input: true,
title: "Rename file",
paragraph: "Enter a new name for the file",
defaultValue: attachment.metadata.filename,
defaultValue: attachment.filename,
positivePress: async (value) => {
if (value && value.length > 0) {
await db.attachments.add({
hash: attachment.metadata.hash,
hash: attachment.hash,
filename: value
});
setFilename(value);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
}
},
positiveText: "Rename"
@@ -151,34 +169,23 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
{
name: "Delete",
onPress: async () => {
await db.attachments.remove(attachment.metadata.hash, false);
await db.attachments.remove(attachment.hash, false);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
close();
},
icon: "delete-outline"
}
];
const getNotes = useCallback(() => {
let allNotes = db.notes.all;
let attachmentNotes = attachment.noteIds?.map((id) => {
let index = allNotes?.findIndex((note) => id === note.id);
if (index !== -1) {
return allNotes[index];
} else {
return {
type: "notfound",
title: `Note with id ${id} does not exist.`,
id: id
};
}
});
return attachmentNotes;
}, [attachment.noteIds]);
useEffect(() => {
setNotes(getNotes());
}, [attachment, getNotes]);
db.relations
.to(attachment, "note")
.selector.items()
.then((items) => {
setNotes(items);
});
}, [attachment]);
return (
<ScrollView
@@ -221,7 +228,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
}}
color={colors.secondary.paragraph}
>
{attachment.metadata.type}
{attachment.type}
</Paragraph>
<Paragraph
style={{
@@ -230,10 +237,10 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{formatBytes(attachment.length)}
{formatBytes(attachment.size)}
</Paragraph>
{attachment.noteIds ? (
{notes.length ? (
<Paragraph
style={{
marginRight: 10
@@ -241,13 +248,13 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{attachment.noteIds.length} note
{attachment.noteIds.length > 1 ? "s" : ""}
{notes.length} note
{notes.length > 1 ? "s" : ""}
</Paragraph>
) : null}
<Paragraph
onPress={() => {
Clipboard.setString(attachment.metadata.hash);
Clipboard.setString(attachment.hash);
ToastManager.show({
type: "success",
heading: "Attachment hash copied",
@@ -257,7 +264,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{attachment.metadata.hash}
{attachment.hash}
</Paragraph>
</View>
@@ -286,21 +293,11 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
{notes.map((item) => (
<PressableButton
onPress={async () => {
if (item.type === "notfound") {
ToastManager.show({
heading: "Note not found",
message:
"A note with the given id was not found. Maybe you have deleted the note or moved it to trash already.",
type: "error",
context: "local"
});
return;
}
eSendEvent(eCloseSheet, contextId);
await sleep(150);
eSendEvent(eCloseAttachmentDialog);
await sleep(300);
openNote(item, item.type === "trash");
openNote(item, (item as any).type === "trash");
}}
customStyle={{
paddingVertical: 12,
@@ -321,9 +318,8 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
<Button
key={item.name}
buttonType={{
text: item.on
? colors.primary.accent
: item.name === "Delete" || item.name === "PermDelete"
text:
item.name === "Delete" || item.name === "PermDelete"
? colors.error.paragraph
: colors.primary.paragraph
}}
@@ -331,7 +327,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
title={item.name}
icon={item.icon}
loading={loading?.name === item.name}
type={item.on ? "shade" : "gray"}
type="gray"
fontSize={SIZE.sm}
style={{
borderRadius: 0,
@@ -359,7 +355,11 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
);
};
Actions.present = (attachment, set, context) => {
Actions.present = (
attachment: Attachment,
set: (attachments?: VirtualizedGrouping<Attachment>) => void,
context?: string
) => {
presentSheet({
context: context,
component: (ref, close) => (

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { formatBytes } from "@notesnook/common";
import React from "react";
import React, { useEffect, useState } from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
@@ -29,32 +29,47 @@ 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) {
function getFileExtension(filename: string) {
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
attachment,
id,
attachments,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading,
context
}: {
id: string;
attachments?: VirtualizedGrouping<Attachment>;
encryption?: boolean;
setAttachments: (attachments: any) => void;
pressable?: boolean;
hideWhenNotDownloading?: boolean;
context?: string;
}) => {
const [attachment] = useDBItem(id, "attachment", attachments?.item);
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const onPress = () => {
if (!pressable) return;
if (!pressable || !attachment) return;
Actions.present(attachment, setAttachments, context);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
return (hideWhenNotDownloading &&
(!currentProgress || !(currentProgress as any).value)) ||
!attachment ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
@@ -67,7 +82,6 @@ export const AttachmentItem = ({
borderRadius: 5,
backgroundColor: colors.secondary.background
}}
type="grayBg"
>
<View
style={{
@@ -93,7 +107,7 @@ export const AttachmentItem = ({
position: "absolute"
}}
>
{getFileExtension(attachment.metadata.filename).toUpperCase()}
{getFileExtension(attachment.filename).toUpperCase()}
</Paragraph>
</View>
@@ -113,14 +127,14 @@ export const AttachmentItem = ({
lineBreakMode="middle"
color={colors.primary.paragraph}
>
{attachment.metadata.filename}
{attachment.filename}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
{formatBytes(attachment.size)}{" "}
{(currentProgress as any)?.type
? "(" + (currentProgress as any).type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
@@ -132,8 +146,8 @@ export const AttachmentItem = ({
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
db.fs().cancel(attachment.metadata.hash);
setCurrentProgress(undefined);
}}
style={{
justifyContent: "center",

View File

@@ -21,7 +21,10 @@ import React, { useRef, useState } from "react";
import { Platform, View } from "react-native";
import { db } from "../../common/database";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import { presentSheet } from "../../services/event-manager";
import {
PresentSheetOptions,
presentSheet
} from "../../services/event-manager";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -29,8 +32,19 @@ import { ProgressBarComponent } from "../ui/svg/lazy";
import { useThemeColors } from "@notesnook/theme";
import { FlatList } from "react-native-actions-sheet";
import { AttachmentItem } from "./attachment-item";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
const DownloadAttachments = ({
close,
attachments,
isNote,
update
}: {
attachments: VirtualizedGrouping<Attachment>;
close?: ((ctx?: string | undefined) => void) | undefined;
isNote?: boolean;
update?: (props: PresentSheetOptions) => void;
}) => {
const { colors } = useThemeColors();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
@@ -39,38 +53,40 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef();
const groupId = useRef<string>();
const onDownload = async () => {
update({
update?.({
disableClosing: true
});
} as PresentSheetOptions);
setDownloading(true);
canceled.current = false;
groupId.current = Date.now().toString();
const result = await downloadAttachments(
attachments,
(progress, statusText) => setProgress({ value: progress, statusText }),
(progress: number, statusText: string) =>
setProgress({ value: progress, statusText }),
canceled,
groupId.current
);
if (canceled.current) return;
setResult(result || new Map());
setDownloading(false);
update({
update?.({
disableClosing: false
});
} as PresentSheetOptions);
};
const cancel = async () => {
update({
update?.({
disableClosing: false
});
} as PresentSheetOptions);
canceled.current = true;
if (!groupId.current) return;
console.log(groupId.current, "canceling groupId downloads");
await db.fs().cancel(groupId.current);
await db.fs().cancel(groupId.current, "download");
setDownloading(false);
groupId.current = null;
groupId.current = undefined;
};
const successResults = () => {
@@ -91,11 +107,11 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
function getResultText() {
const downloadedAttachmentsCount =
attachments.length - failedResults().length;
attachments?.ids?.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments.length
attachments?.ids.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
@@ -157,7 +173,9 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
width={null}
animated={true}
useNativeDriver
progress={progress.value ? progress.value / attachments.length : 0}
progress={
progress.value ? progress.value / attachments.ids?.length : 0
}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
@@ -174,7 +192,7 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments : []}
data={downloading ? attachments.ids : undefined}
ListEmptyComponent={
<View
style={{
@@ -189,14 +207,15 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
</Paragraph>
</View>
}
keyExtractor={(item) => item.id}
keyExtractor={(item) => item as string}
renderItem={({ item }) => {
return (
<AttachmentItem
attachment={item}
id={item as string}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
attachments={attachments}
/>
);
}}
@@ -209,7 +228,9 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
borderRadius: 100,
marginTop: 20
}}
onPress={close}
onPress={() => {
close?.();
}}
type="accent"
title="Done"
/>
@@ -227,7 +248,9 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
borderRadius: 100,
marginRight: 5
}}
onPress={close}
onPress={() => {
close?.();
}}
type="grayBg"
title="No"
/>
@@ -258,7 +281,11 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
);
};
DownloadAttachments.present = (context, attachments, isNote) => {
DownloadAttachments.present = (
context: string,
attachments: VirtualizedGrouping<Attachment>,
isNote?: boolean
) => {
presentSheet({
context: context,
component: (ref, close, update) => (

View File

@@ -17,15 +17,25 @@ 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, { useRef, useState } from "react";
import {
Attachment,
Note,
SortOptions,
VirtualizedGrouping
} from "@notesnook/core";
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import { presentSheet } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { useSettingStore } from "../../stores/use-setting-store";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
@@ -33,82 +43,83 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import DownloadAttachments from "./download-attachments";
import { Button } from "../ui/button";
import {
isAudio,
isDocument,
isImage,
isVideo
} from "@notesnook/core/dist/utils/filename";
import { useSettingStore } from "../../stores/use-setting-store";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
export const AttachmentDialog = ({ note }) => {
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateEdited",
sortDirection: "desc"
};
export const AttachmentDialog = ({ note }: { note?: Note }) => {
const { colors } = useThemeColors();
const { height } = useSettingStore((state) => state.dimensions);
const [attachments, setAttachments] = useState(
note
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])]
);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [attachments, setAttachments] =
useState<VirtualizedGrouping<Attachment>>();
const attachmentSearchValue = useRef<string>();
const searchTimer = useRef<NodeJS.Timeout>();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("all");
const onChangeText = (text) => {
const attachments = note?.id
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])];
const refresh = React.useCallback(() => {
if (note) {
db.attachments.ofNote(note.id, "all").sorted(DEFAULT_SORTING);
} else {
db.attachments.all
.sorted(DEFAULT_SORTING)
.then((attachments) => setAttachments(attachments));
}
}, [note]);
useEffect(() => {
refresh();
}, [note, refresh]);
const onChangeText = (text: string) => {
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments(filterAttachments(currentFilter));
refresh();
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
attachments,
attachmentSearchValue.current
searchTimer.current = setTimeout(async () => {
let results = await db.lookup.attachments(
attachmentSearchValue.current as string
);
if (results.length === 0) return;
setAttachments(filterAttachments(currentFilter, results));
setAttachments(filterAttachments(currentFilter));
setAttachments(results);
}, 300);
};
const renderItem = ({ item }) => (
const renderItem = ({ item }: { item: string }) => (
<AttachmentItem
setAttachments={() => {
setAttachments(filterAttachments(currentFilter));
}}
attachment={item}
attachments={attachments}
id={item}
context="attachments-list"
/>
);
const onCheck = async () => {
if (!attachments) return;
setLoading(true);
const checkedAttachments = [];
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
for (let id of attachments.ids) {
const attachment = await attachments.item(id as string);
if (!attachment) continue;
let result = await filesystem.checkAttachment(attachment.hash);
if (result.failed) {
await db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
await db.attachments.markAsFailed(attachment.hash, result.failed);
} else {
await db.attachments.markAsFailed(attachment.id, null);
await db.attachments.markAsFailed(id as string, undefined);
}
checkedAttachments.push(
db.attachments.attachment(attachment.metadata.hash)
);
setAttachments([...checkedAttachments]);
}
refresh();
setLoading(false);
};
@@ -135,33 +146,33 @@ export const AttachmentDialog = ({ note }) => {
}
];
const filterAttachments = (type, _attachments) => {
const attachments = _attachments
? _attachments
: note
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])];
const filterAttachments = async (type: string) => {
let items: FilteredSelector<Attachment> = db.attachments.all;
switch (type) {
case "all":
return attachments;
items = db.attachments.all;
break;
case "images":
return attachments.filter((attachment) =>
isImage(attachment.metadata.type)
);
items = note
? db.attachments.ofNote(note.id, "images")
: db.attachments.images;
break;
case "video":
return attachments.filter((attachment) =>
isVideo(attachment.metadata.type)
);
items = items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.videos;
break;
case "audio":
return attachments.filter((attachment) =>
isAudio(attachment.metadata.type)
);
items = db.attachments.all;
break;
case "documents":
return attachments.filter((attachment) =>
isDocument(attachment.metadata.type)
);
items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.documents;
}
return await items.sorted(DEFAULT_SORTING);
};
return (
@@ -219,6 +230,7 @@ export const AttachmentDialog = ({ note }) => {
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
DownloadAttachments.present(
"attachments-list",
attachments,
@@ -235,7 +247,7 @@ export const AttachmentDialog = ({ note }) => {
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
onChangeText(attachmentSearchValue.current as string);
}}
/>
@@ -257,10 +269,7 @@ export const AttachmentDialog = ({ note }) => {
<Button
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
}
title={item.title}
style={{
borderRadius: 0,
borderBottomWidth: 1,
@@ -270,9 +279,9 @@ export const AttachmentDialog = ({ note }) => {
? "transparent"
: colors.primary.accent
}}
onPress={() => {
onPress={async () => {
setCurrentFilter(item.filterBy);
setAttachments(filterAttachments(item.filterBy));
setAttachments(await filterAttachments(item.filterBy));
}}
/>
))}
@@ -303,7 +312,7 @@ export const AttachmentDialog = ({ note }) => {
/>
}
estimatedItemSize={50}
data={attachments}
data={attachments?.ids as string[]}
renderItem={renderItem}
/>
@@ -326,7 +335,7 @@ export const AttachmentDialog = ({ note }) => {
);
};
AttachmentDialog.present = (note) => {
AttachmentDialog.present = (note?: Note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
});

View File

@@ -119,7 +119,7 @@ const FloatingButton = ({
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={colors.static[color as keyof typeof colors.static]}
accentColor={color}
customStyle={{
...getElevationStyle(5),
borderRadius: 100

View File

@@ -17,15 +17,17 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeColors } from "@notesnook/theme";
import { AnnouncementDialog } from "../announcements";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog";
import { AddTopicDialog } from "../dialogs/add-topic";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { LoadingDialog } from "../dialogs/loading";
import PDFPreview from "../dialogs/pdf-preview";
import ResultDialog from "../dialogs/result";
import { VaultDialog } from "../dialogs/vault";
import ImagePreview from "../image-preview";
@@ -36,7 +38,6 @@ import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import RestoreDataSheet from "../sheets/restore-data";
import PDFPreview from "../dialogs/pdf-preview";
const DialogProvider = () => {
const { colors } = useThemeColors();
@@ -46,7 +47,6 @@ const DialogProvider = () => {
<>
<LoadingDialog />
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
@@ -62,6 +62,7 @@ const DialogProvider = () => {
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
</>
);
};

View File

@@ -21,11 +21,9 @@ import { eSendEvent } from "../../services/event-manager";
import {
eCloseActionSheet,
eCloseAddNotebookDialog,
eCloseAddTopicDialog,
eCloseMoveNoteDialog,
eOpenActionSheet,
eOpenAddNotebookDialog,
eOpenAddTopicDialog,
eOpenMoveNoteDialog
} from "../../utils/events";
@@ -52,9 +50,3 @@ export const AddNotebookEvent = (notebook) => {
export const HideAddNotebookEvent = (notebook) => {
eSendEvent(eCloseAddNotebookDialog, notebook);
};
export const AddTopicEvent = (topic) => {
eSendEvent(eOpenAddTopicDialog, topic);
};
export const HideAddTopicEvent = (notebook) => {
eSendEvent(eCloseAddTopicDialog, notebook);
};

View File

@@ -1,194 +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 React, { createRef } from "react";
import { View } from "react-native";
import { useMenuStore } from "../../../stores/use-menu-store";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastManager
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { db } from "../../../common/database";
import {
eCloseAddTopicDialog,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogContainer from "../../dialog/dialog-container";
import DialogHeader from "../../dialog/dialog-header";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { Toast } from "../../toast";
import { useRelationStore } from "../../../stores/use-relation-store";
export class AddTopicDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
titleFocused: false,
loading: false
};
this.ref = createRef();
this.title;
this.titleRef = createRef();
this.notebook = null;
this.toEdit = null;
}
addNewTopic = async () => {
try {
if (!this.title || this.title?.trim() === "") {
ToastManager.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return;
}
if (!this.toEdit) {
await db.notebooks.topics(this.notebook.id).add({
title: this.title
});
} else {
let topic = this.toEdit;
topic.title = this.title;
await db.notebooks.topics(topic.notebookId).add({
id: topic.id,
title: topic.title
});
}
this.close();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
useMenuStore.getState().setMenuPins();
});
eSendEvent(eOnTopicSheetUpdate);
useRelationStore.getState().update();
} catch (e) {
console.error(e);
}
};
componentDidMount() {
eSubscribeEvent(eOpenAddTopicDialog, this.open);
eSubscribeEvent(eCloseAddTopicDialog, this.close);
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenAddTopicDialog, this.open);
eUnSubscribeEvent(eCloseAddTopicDialog, this.close);
}
open = async ({ notebookId, toEdit }) => {
let id = notebookId;
if (id) {
this.notebook = await db.notebooks.notebook(id).data;
}
this.toEdit = toEdit;
if (this.toEdit) {
this.title = this.toEdit.title;
}
this.setState({
visible: true
});
};
close = () => {
this.title = null;
this.notebook = null;
this.toEdit = null;
this.setState({
visible: false
});
};
render() {
const { visible } = this.state;
if (!visible) return null;
return (
<BaseDialog
onShow={async () => {
if (this.toEdit) {
this.titleRef.current?.setNativeProps({
text: this.toEdit.title
});
}
this.ref.current?.animateNextTransition();
await sleep(300);
this.titleRef.current?.focus();
}}
bounce={false}
statusBarTranslucent={false}
visible={true}
onRequestClose={this.close}
>
<DialogContainer>
<DialogHeader
icon="book-outline"
title={this.toEdit ? "Edit topic" : "New topic"}
paragraph={
this.toEdit
? "Edit title of the topic"
: "Add a new topic in " + this.notebook.title
}
padding={12}
/>
<Seperator half />
<View
style={{
paddingHorizontal: 12,
zIndex: 10
}}
>
<Input
fwdRef={this.titleRef}
testID="input-title"
onChangeText={(value) => {
this.title = value;
}}
blurOnSubmit={false}
placeholder="Enter title"
onSubmit={() => this.addNewTopic()}
returnKeyLabel="Done"
returnKeyType="done"
/>
</View>
<DialogButtons
positiveTitle={this.toEdit ? "Save" : "Add"}
onPressNegative={() => this.close()}
onPressPositive={() => this.addNewTopic()}
/>
</DialogContainer>
<Toast context="local" />
</BaseDialog>
);
}
}

View File

@@ -17,15 +17,22 @@ 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, { useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import { GroupHeader, Item, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState
} from "react";
import { FlatList, ScrollView, View } from "react-native";
import { DDS } from "../../../services/device-detection";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useMessageStore } from "../../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseJumpToDialog,
@@ -36,27 +43,47 @@ import { SIZE } from "../../../utils/size";
import BaseDialog from "../../dialog/base-dialog";
import { PressableButton } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { useCallback } from "react";
const offsets = [];
let timeout = null;
const JumpToSectionDialog = ({ scrollRef, data, type }) => {
const JumpToSectionDialog = () => {
const scrollRef = useRef<RefObject<FlatList>>();
const [data, setData] = useState<VirtualizedGrouping<Item>>();
const { colors } = useThemeColors();
const notes = data;
const [visible, setVisible] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const offsets = useRef<number[]>([]);
const timeout = useRef<NodeJS.Timeout>();
const onPress = (item) => {
let ind = notes.findIndex(
(i) => i.title === item.title && i.type === "header"
);
scrollRef.current?.scrollToIndex({
index: ind,
const onPress = (item: GroupHeader) => {
const index = notes?.ids?.findIndex((i) => {
if (typeof i === "object") {
return i.title === item.title && i.type === "header";
} else {
false;
}
});
scrollRef.current?.current?.scrollToIndex({
index: index as number,
animated: true
});
close();
};
const open = useCallback(
({
data,
ref
}: {
data: VirtualizedGrouping<Item>;
ref: RefObject<FlatList>;
}) => {
setData(data);
scrollRef.current = ref;
setVisible(true);
},
[]
);
useEffect(() => {
eSubscribeEvent(eOpenJumpToDialog, open);
eSubscribeEvent(eCloseJumpToDialog, close);
@@ -69,50 +96,52 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
};
}, [open]);
const onScroll = (data) => {
let y = data.y;
const onScroll = (data: { x: number; y: number }) => {
const y = data.y;
if (timeout) {
clearTimeout(timeout);
timeout = null;
clearTimeout(timeout.current);
timeout.current = undefined;
}
timeout = setTimeout(() => {
let index = offsets.findIndex((o, i) => o <= y && offsets[i + 1] > y);
setCurrentIndex(index || 0);
timeout.current = setTimeout(() => {
setCurrentIndex(
offsets.current?.findIndex(
(o, i) => o <= y && offsets.current[i + 1] > y
) || 0
);
}, 200);
};
const open = useCallback(
(_type) => {
if (_type !== type) return;
setVisible(true);
},
[type]
);
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;
let offset = 35 * index;
let ind = notes.ids.findIndex(
(i) =>
typeof i === "object" &&
i.title === item.title &&
i.type === "header"
);
const messageState = useMessageStore.getState().message;
const msgOffset = messageState?.visible ? 60 : 10;
ind = ind + 1;
ind = ind - (index + 1);
offset = offset + ind * 100 + msgOffset;
offsets.current.push(offset);
});
}, [notes]);
useEffect(() => {
loadOffsets();
}, [loadOffsets, notes]);
const loadOffsets = useCallback(() => {
notes
.filter((i) => i.type === "header")
.map((item, index) => {
let offset = 35 * index;
let ind = notes.findIndex(
(i) => i.title === item.title && i.type === "header"
);
let messageState = useMessageStore.getState().message;
let msgOffset = messageState?.visible ? 60 : 10;
ind = ind + 1;
ind = ind - (index + 1);
offset = offset + ind * 100 + msgOffset;
offsets.push(offset);
});
}, [notes]);
return !visible ? null : (
<BaseDialog
onShow={() => {
@@ -149,13 +178,13 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
paddingBottom: 20
}}
>
{notes
.filter((i) => i.type === "header")
{notes?.ids
.filter((i) => typeof i === "object" && i.type === "header")
.map((item, index) => {
return item.title ? (
return typeof item === "object" && item.title ? (
<PressableButton
key={item.title}
onPress={() => onPress(item, index)}
onPress={() => onPress(item)}
type={currentIndex === index ? "selected" : "transparent"}
customStyle={{
minWidth: "20%",

View File

@@ -42,7 +42,6 @@ const ImagePreview = () => {
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
return () => {
eUnSubscribeEvent("ImagePreview", open);
};

View File

@@ -17,57 +17,39 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { useMessageStore } from "../../../stores/use-message-store";
import { ColorValues } from "../../../utils/colors";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { Announcement } from "../../announcements/announcement";
import { Card } from "../../list/card";
import Paragraph from "../../ui/typography/paragraph";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { SIZE } from "../../../utils/size";
import { useSelectionStore } from "../../../stores/use-selection-store";
export type ListHeaderProps = {
noAnnouncement?: boolean;
color?: string;
messageCard?: boolean;
screen?: string;
shouldShow?: boolean;
};
export const Header = React.memo(
({
type,
messageCard = true,
color,
shouldShow = false,
noAnnouncement,
warning
}) => {
screen
}: ListHeaderProps) => {
const { colors } = useThemeColors();
const announcements = useMessageStore((state) => state.announcements);
const selectionMode = useSelectionStore((state) => state.selectionMode);
return selectionMode ? null : (
<>
{warning ? (
<View
style={{
padding: 12,
backgroundColor: colors.error.background,
width: "95%",
alignSelf: "center",
borderRadius: 5,
flexDirection: "row",
alignItems: "center"
}}
>
<Icon
name="sync-alert"
size={SIZE.md}
color={colors.error.icon}
f
/>
<Paragraph style={{ marginLeft: 5 }} color={colors.error.icon}>
{warning.title}
</Paragraph>
</View>
) : announcements.length !== 0 && !noAnnouncement ? (
{announcements.length !== 0 && !noAnnouncement ? (
<Announcement color={color || colors.primary.accent} />
) : type === "search" ? null : !shouldShow ? (
) : (screen as any) === "Search" ? null : !shouldShow ? (
<View
style={{
marginBottom: 5,
@@ -78,11 +60,7 @@ export const Header = React.memo(
}}
>
{messageCard ? (
<Card
color={
ColorValues[color?.toLowerCase()] || colors.primary.accent
}
/>
<Card color={color || colors.primary.accent} />
) : null}
</View>
) : null}

View File

@@ -17,27 +17,33 @@ 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 { useRef, useState } from "react";
import React from "react";
import { View } from "react-native";
import { Notebook } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { useMenuStore } from "../../../stores/use-menu-store";
import { ToastManager } from "../../../services/event-manager";
import { getTotalNotes } from "@notesnook/common";
import React, { useState } from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import { ToastManager } from "../../../services/event-manager";
import { useMenuStore } from "../../../stores/use-menu-store";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
export const NotebookHeader = ({ notebook, onEditNotebook }) => {
export const NotebookHeader = ({
notebook,
onEditNotebook,
totalNotes = 0
}: {
notebook: Notebook;
onEditNotebook: () => void;
totalNotes: number;
}) => {
const { colors } = useThemeColors();
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
db.shortcuts.exists(notebook.id)
);
const setMenuPins = useMenuStore((state) => state.setMenuPins);
const totalNotes = getTotalNotes(notebook);
const shortcutRef = useRef();
const onPinNotebook = async () => {
try {
@@ -76,7 +82,7 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
}}
>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{new Date(notebook.dateEdited).toLocaleString()}
{getFormattedDate(notebook.dateModified, "date-time")}
</Paragraph>
<View
style={{
@@ -103,7 +109,6 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
name={isPinnedToMenu ? "link-variant-off" : "link-variant"}
onPress={onPinNotebook}
tooltipText={"Create shortcut in side menu"}
fwdRef={shortcutRef}
customStyle={{
marginRight: 15,
width: 40,
@@ -138,15 +143,15 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
style={{
marginTop: 10,
fontStyle: "italic",
fontFamily: null
fontFamily: undefined
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{notebook.topics.length === 1
{/* {notebook.topics.length === 1
? "1 topic"
: `${notebook.topics.length} topics`}
,{" "}
,{" "} */}
{notebook && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1

View File

@@ -17,39 +17,52 @@ 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, GroupOptions, ItemType } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef } from "react";
import React from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { eSendEvent, presentSheet } from "../../../services/event-manager";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { presentSheet } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { RouteName } from "../../../stores/use-navigation-store";
import { ColorValues } from "../../../utils/colors";
import { GROUP } from "../../../utils/constants";
import { eOpenJumpToDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import Sort from "../../sheets/sort";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
export const SectionHeader = React.memo(
function SectionHeader({ item, index, type, color, screen, groupOptions }) {
type SectionHeaderProps = {
item: GroupHeader;
index: number;
dataType: ItemType;
color?: string;
screen?: RouteName;
groupOptions: GroupOptions;
onOpenJumpToDialog: () => void;
};
export const SectionHeader = React.memo<
React.FunctionComponent<SectionHeaderProps>
>(
function SectionHeader({
item,
index,
dataType,
color,
screen,
groupOptions,
onOpenJumpToDialog
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const { fontScale } = useWindowDimensions();
let groupBy = Object.keys(GROUP).find(
(key) => GROUP[key] === groupOptions.groupBy
(key) => GROUP[key as keyof typeof GROUP] === groupOptions.groupBy
);
const jumpToRef = useRef();
const sortRef = useRef();
const compactModeRef = useRef();
const notebooksListMode = useSettingStore(
(state) => state.settings.notebooksListMode
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
);
const notesListMode = useSettingStore(
(state) => state.settings.notesListMode
);
const listMode = type === "notebooks" ? notebooksListMode : notesListMode;
groupBy = !groupBy
? "Default"
@@ -72,9 +85,8 @@ export const SectionHeader = React.memo(
>
<TouchableOpacity
onPress={() => {
eSendEvent(eOpenJumpToDialog, type);
onOpenJumpToDialog();
}}
ref={jumpToRef}
activeOpacity={0.9}
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
style={{
@@ -83,7 +95,10 @@ export const SectionHeader = React.memo(
}}
>
<Heading
color={ColorValues[color?.toLowerCase()] || colors.primary.accent}
color={
ColorValues[color?.toLowerCase() as keyof typeof ColorValues] ||
colors.primary.accent
}
size={SIZE.sm}
style={{
minWidth: 60,
@@ -106,11 +121,10 @@ export const SectionHeader = React.memo(
<Button
onPress={() => {
presentSheet({
component: <Sort screen={screen} type={type} />
component: <Sort screen={screen} type={dataType} />
});
}}
tooltipText="Change sorting of items in list"
fwdRef={sortRef}
title={groupBy}
icon={
groupOptions.sortDirection === "asc"
@@ -123,7 +137,9 @@ export const SectionHeader = React.memo(
paddingHorizontal: 0,
backgroundColor: "transparent",
marginRight:
type === "notes" || type === "home" || type === "notebooks"
dataType === "note" ||
screen === "Notes" ||
dataType === "notebook"
? 10
: 0
}}
@@ -137,24 +153,26 @@ export const SectionHeader = React.memo(
height: 25
}}
hidden={
type !== "notes" && type !== "notebooks" && type !== "home"
dataType !== "note" &&
dataType !== "notebook" &&
screen !== "Notes"
}
testID="icon-compact-mode"
tooltipText={
listMode == "compact"
isCompactModeEnabled
? "Switch to normal mode"
: "Switch to compact mode"
}
fwdRef={compactModeRef}
color={colors.secondary.icon}
name={listMode == "compact" ? "view-list" : "view-list-outline"}
name={isCompactModeEnabled ? "view-list" : "view-list-outline"}
onPress={() => {
let settings = {};
settings[
type !== "notebooks" ? "notesListMode" : "notebooksListMode"
] = listMode === "normal" ? "compact" : "normal";
SettingsService.set(settings);
SettingsService.set({
[dataType !== "notebook"
? "notesListMode"
: "notebooksListMode"]: isCompactModeEnabled
? "compact"
: "normal"
});
}}
size={SIZE.lg - 2}
/>
@@ -166,7 +184,6 @@ export const SectionHeader = React.memo(
},
(prev, next) => {
if (prev.item.title !== next.item.title) return false;
return true;
}
);

View File

@@ -17,21 +17,30 @@ 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 { getUpcomingReminder } from "@notesnook/core/dist/collections/reminders";
import { decode, EntityLevel } from "entities";
import {
BaseTrashItem,
Color,
Note,
Reminder,
TrashItem
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { EntityLevel, decode } from "entities";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import NotebookScreen from "../../../screens/notebook";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import { useEditorStore } from "../../../stores/use-editor-store";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../../utils/colors";
import { SIZE } from "../../../utils/size";
import {
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../list/list-item.wrapper";
import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
@@ -39,73 +48,39 @@ import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useEditorStore } from "../../../stores/use-editor-store";
function navigateToTag(item) {
const tag = db.tags.tag(item.id);
if (!tag) return;
TaggedNotes.navigate(tag, true);
}
const showActionSheet = (item) => {
Properties.present(item);
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
index: number;
tags?: TagsWithDateEdited;
notebooks?: NotebooksWithDateEdited;
color?: Color;
reminder?: Reminder;
attachmentsCount: number;
date: number;
isTrash?: boolean;
noOpen?: boolean;
};
function getNotebook(item) {
const isTrash = item.type === "trash";
const currentId = useNavigationStore.getState().currentScreen.id;
if (isTrash) return [];
const items = [];
const notebooks = db.relations.to(item, "notebook") || [];
for (let notebook of notebooks) {
if (items.length > 1) break;
if (notebook.id === currentId) continue;
items.push(notebook);
}
if (item.notebooks) {
for (let nb of item.notebooks) {
if (items.length > 1) break;
const notebook = db.notebooks?.notebook(nb.id)?.data;
if (!notebook) continue;
for (let topicId of nb.topics) {
if (items.length > 1) break;
if (topicId === currentId) continue;
const topic = notebook.topics.find((t) => t.id === topicId);
if (!topic) continue;
items.push(topic);
}
}
}
return items;
}
function getTags(item) {
const noteTags = db.relations.to(item, "tag").resolved();
return noteTags;
}
const NoteItem = ({
item,
isTrash,
dateBy = "dateCreated",
date,
color,
notebooks,
reminder,
tags,
attachmentsCount,
noOpen = false
}) => {
}: NoteItemProps) => {
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
);
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(item);
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const _update = useRelationStore((state) => state.updater);
// eslint-disable-next-line react-hooks/exhaustive-deps
const notebooks = React.useMemo(() => getNotebook(item), [item, _update]);
const reminders = db.relations.from(item, "reminder");
const reminder = getUpcomingReminder(reminders);
const noteColor = ColorValues[item.color?.toLowerCase()];
const tags = getTags(item);
const primaryColors = isEditingNote ? colors.selected : colors.primary;
return (
@@ -127,7 +102,12 @@ const NoteItem = ({
flexWrap: "wrap"
}}
>
{notebooks?.map((item) => (
{notebooks?.items
?.filter(
(item) =>
item.id !== useNavigationStore.getState().currentScreen?.id
)
.map((item) => (
<Button
title={
item.title.length > 25
@@ -137,7 +117,7 @@ const NoteItem = ({
tooltipText={item.title}
key={item.id}
height={25}
icon={item.type === "topic" ? "bookmark" : "book-outline"}
icon={item.type === "notebook" ? "bookmark" : "book-outline"}
type="grayBg"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
@@ -153,18 +133,14 @@ const NoteItem = ({
marginBottom: 5
}}
onPress={() => {
if (item.type === "topic") {
TopicNotes.navigate(item, true);
} else {
NotebookScreen.navigate(item);
}
}}
/>
))}
<ReminderTime
reminder={reminder}
color={noteColor}
color={color?.colorCode}
onPress={() => {
Properties.present(reminder);
}}
@@ -178,9 +154,7 @@ const NoteItem = ({
{compactMode ? (
<Paragraph
numberOfLines={1}
color={
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
color={color?.colorCode || primaryColors.heading}
style={{
flexWrap: "wrap"
}}
@@ -191,9 +165,7 @@ const NoteItem = ({
) : (
<Heading
numberOfLines={1}
color={
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
color={color?.colorCode || primaryColors.heading}
style={{
flexWrap: "wrap"
}}
@@ -246,13 +218,11 @@ const NoteItem = ({
color: colors.secondary.paragraph,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/>
{attachmentCount > 0 ? (
{attachmentsCount > 0 ? (
<View
style={{
flexDirection: "row",
@@ -269,7 +239,7 @@ const NoteItem = ({
color={colors.secondary.paragraph}
size={SIZE.xs}
>
{attachmentCount}
{attachmentsCount}
</Paragraph>
</View>
) : null}
@@ -282,10 +252,7 @@ const NoteItem = ({
style={{
marginRight: 6
}}
color={
ColorValues[item.color?.toLowerCase()] ||
primaryColors.accent
}
color={color?.colorCode || primaryColors.accent}
/>
) : null}
@@ -314,7 +281,7 @@ const NoteItem = ({
) : null}
{!isTrash && !compactMode && tags
? tags.map((item) =>
? tags.items?.map((item) =>
item.id ? (
<Button
title={"#" + item.title}
@@ -331,9 +298,9 @@ const NoteItem = ({
paddingHorizontal: 6,
marginRight: 4,
zIndex: 10,
maxWidth: tags.length > 1 ? 130 : null
maxWidth: tags.items?.length > 1 ? 130 : null
}}
onPress={() => navigateToTag(item)}
onPress={() => TaggedNotes.navigate(item, true)}
/>
) : null
)
@@ -361,7 +328,8 @@ const NoteItem = ({
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
</Paragraph>
</>
)}
@@ -417,8 +385,8 @@ const NoteItem = ({
color: colors.secondary.paragraph,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/>
</>
) : null}
@@ -428,7 +396,7 @@ const NoteItem = ({
color={primaryColors.paragraph}
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && showActionSheet(item, isTrash)}
onPress={() => !noOpen && Properties.present(item)}
customStyle={{
justifyContent: "center",
height: 35,

View File

@@ -17,6 +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 { BaseTrashItem, Color, Note, Reminder } from "@notesnook/core";
import React from "react";
import NoteItem from ".";
import { notesnook } from "../../../../e2e/test.ids";
@@ -32,50 +33,43 @@ import { useEditorStore } from "../../../stores/use-editor-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import { presentDialog } from "../../dialog/functions";
import type {
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../list/list-item.wrapper";
import NotePreview from "../../note-history/preview";
import SelectionWrapper from "../selection-wrapper";
const present = () =>
presentDialog({
title: "Note not synced",
negativeText: "Ok",
paragraph: "Please sync again to open this note for editing"
});
export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
let _note = item;
export const openNote = async (
item: Note,
isTrash?: boolean,
isSheet?: boolean
) => {
let note: Note = item;
if (isSheet) hideSheet();
if (!isTrash) {
_note = db.notes.note(item.id).data;
if (!db.notes.note(item.id)?.synced()) {
present();
return;
note = (await db.notes.note(item.id)) as Note;
}
} else {
if (!db.trash.synced(item.id)) {
present();
return;
}
}
const { selectedItemsList, selectionMode, clearSelection } =
const { selectedItemsList, selectionMode, clearSelection, setSelectedItem } =
useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode) {
setSelectedItem && setSelectedItem(_note);
setSelectedItem(note);
return;
} else {
clearSelection();
}
if (_note.conflicted) {
eSendEvent(eShowMergeDialog, _note);
if (note.conflicted) {
eSendEvent(eShowMergeDialog, note);
return;
}
if (_note.locked) {
if (note.locked) {
openVault({
item: _note,
item: note,
novault: true,
locked: true,
goToEditor: true,
@@ -85,16 +79,18 @@ export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
return;
}
if (isTrash) {
const content = await db.content.get(item.contentId);
if (!note.contentId) return;
const content = await db.content.get(note.contentId as string);
presentSheet({
component: (
<NotePreview note={item} content={{ type: "tiptap", data: content }} />
)
});
} else {
useEditorStore.getState().setReadonly(_note?.readonly);
useEditorStore.getState().setReadonly(note?.readonly);
eSendEvent(eOnLoadNote, {
item: _note
item: note
});
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
@@ -102,33 +98,62 @@ export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
}
};
export const NoteWrapper = React.memo(
function NoteWrapper({ item, index, dateBy, isSheet }) {
type NoteWrapperProps = {
item: Note | BaseTrashItem<Note>;
index: number;
tags?: TagsWithDateEdited;
notebooks?: NotebooksWithDateEdited;
color?: Color;
reminder?: Reminder;
attachmentsCount: number;
date: number;
isRenderedInActionSheet: boolean;
};
export const NoteWrapper = React.memo<
React.FunctionComponent<NoteWrapperProps>
>(
function NoteWrapper({
item,
index,
isRenderedInActionSheet,
...restProps
}: NoteWrapperProps) {
const isTrash = item.type === "trash";
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
return (
<SelectionWrapper
index={index}
height={100}
testID={notesnook.ids.note.get(index)}
onPress={() => openNote(item, isTrash, setSelectedItem, isSheet)}
isSheet={isSheet}
onPress={() => openNote(item as Note, isTrash, isRenderedInActionSheet)}
isSheet={isRenderedInActionSheet}
item={item}
color={restProps.color?.colorCode}
>
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} />
<NoteItem {...restProps} item={item} index={index} isTrash={isTrash} />
</SelectionWrapper>
);
},
(prev, next) => {
if (prev.dateBy !== next.dateBy) {
return false;
}
if (prev.item?.dateEdited !== next.item?.dateEdited) {
if (prev.date !== next.date) {
return false;
}
if (prev.item !== next.item) {
if (
prev.tags?.dateEdited !== next.tags?.dateEdited ||
prev.tags?.items?.length !== next.tags?.items?.length
)
return false;
if (
prev.notebooks?.dateEdited !== next.notebooks?.dateEdited ||
prev.notebooks?.items?.length !== next.notebooks?.items?.length
)
return false;
if (prev.color !== next.color) return false;
if (prev.reminder?.id !== next.reminder?.id) return false;
if (prev.attachmentsCount !== next.attachmentsCount) return false;
if (prev.item?.dateModified !== next.item?.dateModified) {
return false;
}

View File

@@ -17,41 +17,38 @@ 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 { getFormattedDate } from "@notesnook/common";
import { BaseTrashItem, Notebook, TrashItem } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
const showActionSheet = (item) => {
Properties.present(item);
};
const navigateToTopic = (topic) => {
if (useSelectionStore.getState().selectedItemsList.length > 0) return;
TopicNotes.navigate(topic, true);
type NotebookItemProps = {
item: Notebook | BaseTrashItem<Notebook>;
totalNotes: number;
date: number;
index: number;
isTrash: boolean;
};
export const NotebookItem = ({
item,
isTopic = false,
isTrash,
dateBy,
date,
totalNotes
}) => {
}: NotebookItemProps) => {
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(item);
const topics = item.topics?.slice(0, 3) || [];
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
return (
<>
@@ -83,7 +80,7 @@ export const NotebookItem = ({
</Heading>
)}
{isTopic || !item.description || compactMode ? null : (
{!item.description || compactMode ? null : (
<Paragraph
size={SIZE.sm}
numberOfLines={2}
@@ -95,42 +92,6 @@ export const NotebookItem = ({
</Paragraph>
)}
{isTopic || compactMode ? null : (
<View
style={{
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap"
}}
>
{topics.map((topic) => (
<Button
title={topic.title}
key={topic.id}
height={null}
textStyle={{
marginRight: 0
}}
type="grayBg"
fontSize={SIZE.xs}
icon="bookmark-outline"
iconSize={SIZE.sm}
style={{
borderRadius: 5,
maxWidth: 120,
borderWidth: 0.5,
paddingVertical: 2.5,
borderColor: colors.primary.border,
paddingHorizontal: 6,
marginVertical: 5,
marginRight: 5
}}
onPress={() => navigateToTopic(topic)}
/>
))}
</View>
)}
{!compactMode ? (
<View
style={{
@@ -152,7 +113,9 @@ export const NotebookItem = ({
}}
>
{"Deleted on " +
new Date(item.dateDeleted).toISOString().slice(0, 10)}
new Date((item as TrashItem).dateDeleted)
.toISOString()
.slice(0, 10)}
</Paragraph>
<Paragraph
color={colors.primary.accent}
@@ -162,7 +125,8 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
</Paragraph>
</>
) : (
@@ -173,7 +137,7 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{getFormattedDate(item[dateBy], "date")}
{getFormattedDate(date, "date")}
</Paragraph>
)}
<Paragraph
@@ -233,7 +197,7 @@ export const NotebookItem = ({
name="dots-horizontal"
testID={notesnook.ids.notebook.menu}
size={SIZE.xl}
onPress={() => showActionSheet(item)}
onPress={() => Properties.present(item)}
customStyle={{
justifyContent: "center",
height: 35,

View File

@@ -17,18 +17,18 @@ 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 { BaseTrashItem, Notebook } from "@notesnook/core";
import React from "react";
import { NotebookItem } from ".";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import { db } from "../../../common/database";
import { ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useTrashStore } from "../../../stores/use-trash-store";
import { db } from "../../../common/database";
import { presentDialog } from "../../dialog/functions";
import SelectionWrapper from "../selection-wrapper";
const navigateToNotebook = (item, canGoBack) => {
const navigateToNotebook = (item: Notebook, canGoBack?: boolean) => {
if (!item) return;
Navigation.navigate(
@@ -46,7 +46,7 @@ const navigateToNotebook = (item, canGoBack) => {
);
};
export const openNotebookTopic = (item) => {
export const openNotebookTopic = (item: Notebook | BaseTrashItem<Notebook>) => {
const isTrash = item.type === "trash";
const { selectedItemsList, setSelectedItem, selectionMode, clearSelection } =
useSelectionStore.getState();
@@ -85,29 +85,30 @@ export const openNotebookTopic = (item) => {
});
return;
}
if (item.type === "topic") {
TopicNotes.navigate(item, true);
} else {
navigateToNotebook(item, true);
}
};
type NotebookWrapperProps = {
item: Notebook | BaseTrashItem<Notebook>;
totalNotes: number;
date: number;
index: number;
};
export const NotebookWrapper = React.memo(
function NotebookWrapper({ item, index, dateBy, totalNotes }) {
function NotebookWrapper({
item,
index,
date,
totalNotes
}: NotebookWrapperProps) {
const isTrash = item.type === "trash";
return (
<SelectionWrapper
pinned={item.pinned}
index={index}
onPress={() => openNotebookTopic(item)}
height={item.type === "topic" ? 80 : 110}
item={item}
>
<SelectionWrapper onPress={() => openNotebookTopic(item)} item={item}>
<NotebookItem
isTopic={item.type === "topic"}
item={item}
dateBy={dateBy}
date={date}
index={index}
isTrash={isTrash}
totalNotes={totalNotes}
@@ -117,17 +118,8 @@ export const NotebookWrapper = React.memo(
},
(prev, next) => {
if (prev.totalNotes !== next.totalNotes) return false;
if (prev.item.title !== next.item.title) return false;
if (prev.dateBy !== next.dateBy) {
return false;
}
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
return false;
}
if (prev.date !== next.date) return false;
if (prev.item?.dateModified !== next.item?.dateModified) return false;
return true;
}

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { View } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import type { Reminder } from "../../../services/notifications";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
@@ -30,6 +29,7 @@ import SelectionWrapper from "../selection-wrapper";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import ReminderSheet from "../../sheets/reminder";
import { ReminderTime } from "../../ui/reminder-time";
import { Reminder } from "@notesnook/core";
const ReminderItem = React.memo(
({
@@ -131,7 +131,11 @@ const ReminderItem = React.memo(
height: 30
}}
>
<Icon name="reload" size={SIZE.md} color={colors.primary.accent} />
<Icon
name="reload"
size={SIZE.md}
color={colors.primary.accent}
/>
<Paragraph
size={SIZE.xs}
color={colors.secondary.paragraph}

View File

@@ -22,8 +22,9 @@ import React from "react";
import { View } from "react-native";
import useIsSelected from "../../../hooks/use-selected";
import { useEditorStore } from "../../../stores/use-editor-store";
import { Item } from "@notesnook/core";
export const Filler = ({ item }) => {
export const Filler = ({ item, color }: { item: Item; color?: string }) => {
const { colors } = useThemeColors();
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id

View File

@@ -18,24 +18,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useRef } from "react";
import React, { PropsWithChildren, useRef } from "react";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { PressableButton } from "../../ui/pressable";
import { Filler } from "./back-fill";
import { SelectionIcon } from "./selection";
import { Item, TrashItem } from "@notesnook/core";
type SelectionWrapperProps = PropsWithChildren<{
item: Item;
onPress: () => void;
testID?: string;
isSheet?: boolean;
color?: string;
}>;
const SelectionWrapper = ({
children,
item,
background,
onPress,
testID,
isSheet
}) => {
isSheet,
children,
color
}: SelectionWrapperProps) => {
const itemId = useRef(item.id);
const { colors, isDark } = useThemeColors();
const compactMode = useIsCompactModeEnabled(item);
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
if (item.id !== itemId.current) {
itemId.current = item.id;
@@ -43,9 +54,6 @@ const SelectionWrapper = ({
const onLongPress = () => {
if (!useSelectionStore.getState().selectionMode) {
useSelectionStore.setState({
selectedItemsList: []
});
useSelectionStore.getState().setSelectionMode(true);
}
useSelectionStore.getState().setSelectedItem(item);
@@ -72,14 +80,8 @@ const SelectionWrapper = ({
marginBottom: isSheet ? 12 : undefined
}}
>
{item.type === "note" ? (
<Filler background={background} item={item} />
) : null}
<SelectionIcon
compactMode={compactMode}
item={item}
onLongPress={onLongPress}
/>
{item.type === "note" ? <Filler item={item} color={color} /> : null}
<SelectionIcon item={item} />
{children}
</PressableButton>
);

View File

@@ -25,13 +25,16 @@ import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enab
import useIsSelected from "../../../hooks/use-selected";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { SIZE } from "../../../utils/size";
import { Item, TrashItem } from "@notesnook/core";
export const SelectionIcon = ({ item }) => {
export const SelectionIcon = ({ item }: { item: Item }) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const [selected] = useIsSelected(item);
const compactMode = useIsCompactModeEnabled(item);
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
return selectionMode ? (
<View

View File

@@ -17,34 +17,41 @@ 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 { Tag } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { db } from "../../../common/database";
const TagItem = React.memo(
({ item, index }) => {
({
item,
index,
totalNotes
}: {
item: Tag;
index: number;
totalNotes: number;
}) => {
const { colors, isDark } = useThemeColors();
const onPress = () => {
TaggedNotes.navigate(item, true);
};
const relations = db.relations.from(item, "note");
return (
<PressableButton
onPress={onPress}
selectedColor={colors.secondary.background}
customSelectedColor={colors.secondary.background}
testID={notesnook.ids.tag.get(index)}
alpha={!isDark ? -0.02 : 0.02}
opacity={1}
customAlpha={!isDark ? -0.02 : 0.02}
customOpacity={1}
customStyle={{
paddingHorizontal: 12,
flexDirection: "row",
@@ -77,10 +84,10 @@ const TagItem = React.memo(
marginTop: 5
}}
>
{relations.length && relations.length > 1
? relations.length + " notes"
: relations.length === 1
? relations.length + " note"
{totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: null}
</Paragraph>
</View>
@@ -105,10 +112,7 @@ const TagItem = React.memo(
);
},
(prev, next) => {
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
if (prev.item?.dateModified !== next.item?.dateModified) {
return false;
}

View File

@@ -27,14 +27,15 @@ import { SIZE } from "../../utils/size";
import { PressableButton } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
export const Card = ({ color, warning }) => {
export const Card = ({ color }: { color?: string }) => {
const { colors } = useThemeColors();
color = color ? color : colors.primary.accent;
const messageBoardState = useMessageStore((state) => state.message);
const announcement = useMessageStore((state) => state.announcement);
const announcements = useMessageStore((state) => state.announcements);
const fontScale = Dimensions.get("window").fontScale;
return !messageBoardState.visible || announcement || warning ? null : (
return !messageBoardState.visible ||
(announcements && announcements.length) ? null : (
<View
style={{
width: "95%"

View File

@@ -17,14 +17,13 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ActivityIndicator, useWindowDimensions, View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useTip } from "../../services/tip-manager";
import { TTip, useTip } from "../../services/tip-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../utils/colors";
import { SIZE } from "../../utils/size";
import { Tip } from "../tip";
import { Button } from "../ui/button";
@@ -32,14 +31,33 @@ import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export type PlaceholderData = {
title: string;
paragraph: string;
button?: string;
action?: () => void;
loading?: string;
type?: string;
};
type EmptyListProps = {
loading?: boolean;
placeholder?: PlaceholderData;
title?: string;
color?: string;
dataType: string;
screen?: string;
};
export const Empty = React.memo(
function Empty({
loading = true,
placeholderData,
headerProps,
type,
placeholder,
title,
color,
dataType,
screen
}) {
}: EmptyListProps) {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const { height } = useWindowDimensions();
@@ -50,8 +68,8 @@ export const Empty = React.memo(
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholderData.type || type,
screen === "Notes" ? "notes" : null
: placeholder?.type || ((dataType + "s") as any),
screen === "Notes" ? "notes" : "list"
);
return (
@@ -68,28 +86,23 @@ export const Empty = React.memo(
{!loading ? (
<>
<Tip
color={
ColorValues[headerProps.color?.toLowerCase()]
? headerProps.color
: "accent"
}
tip={tip || { text: placeholderData.paragraph }}
color={color ? color : "accent"}
tip={tip || ({ text: placeholder?.paragraph } as TTip)}
style={{
backgroundColor: "transparent",
paddingHorizontal: 0
}}
/>
{placeholderData.button && (
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="grayAccent"
title={placeholderData.button}
title={placeholder?.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholderData.action}
onPress={placeholder?.action}
buttonType={{
text:
colors.static[headerProps.color] || colors.primary.accent
text: color || colors.primary.accent
}}
style={{
alignSelf: "flex-start",
@@ -108,17 +121,14 @@ export const Empty = React.memo(
width: "100%"
}}
>
<Heading>{placeholderData.heading}</Heading>
<Heading>{placeholder?.title}</Heading>
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
{placeholderData.loading}
{placeholder?.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={SIZE.lg}
color={
ColorValues[headerProps.color?.toLowerCase()] ||
colors.primary.accent
}
color={color || colors.primary.accent}
/>
</View>
</>

View File

@@ -17,177 +17,169 @@ 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 { getTotalNotes } from "@notesnook/common";
import {
GroupHeader,
GroupingKey,
Item,
VirtualizedGrouping,
isGroupHeader
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { FlashList } from "@shopify/flash-list";
import React, { useEffect, useRef } from "react";
import { RefreshControl, View } from "react-native";
import {
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl
} from "react-native";
import Animated, { FadeInDown } from "react-native-reanimated";
import { notesnook } from "../../../e2e/test.ids";
import { useGroupOptions } from "../../hooks/use-group-options";
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 { eScrollEvent } from "../../utils/events";
import { eOpenJumpToDialog, eScrollEvent } from "../../utils/events";
import { tabBarRef } from "../../utils/global-refs";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { Footer } from "../list-items/footer";
import { Header } from "../list-items/headers/header";
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";
import { Empty } from "./empty";
import { Empty, PlaceholderData } from "./empty";
import { ListItemWrapper } from "./list-item.wrapper";
const renderItems = {
note: NoteWrapper,
notebook: NotebookWrapper,
topic: NotebookWrapper,
tag: TagItem,
section: SectionHeader,
header: SectionHeader,
reminder: ReminderItem
type ListProps = {
data: VirtualizedGrouping<Item> | undefined;
dataType: Item["type"];
onRefresh?: () => void;
loading?: boolean;
headerTitle?: string;
customAccentColor?: string;
renderedInRoute?: RouteName;
CustomLisHeader?: React.JSX.Element;
isRenderedInActionSheet?: boolean;
CustomListComponent?: React.JSX.ElementType;
placeholder?: PlaceholderData;
};
const RenderItem = ({ item, index, type, ...restArgs }) => {
if (!item) return <View />;
const Item = renderItems[item.itemType || item.type] || View;
const totalNotes = getTotalNotes(item);
return (
<Item
item={item}
index={index}
type={type}
totalNotes={totalNotes}
{...restArgs}
/>
);
};
/**
*
* @param {any} param0
* @returns
*/
const List = ({
listData,
type,
refreshCallback,
placeholderData,
loading,
headerProps = {
heading: "Home",
color: null
},
screen,
ListHeader,
warning,
isSheet = false,
onMomentumScrollEnd,
handlers,
ScrollComponent
}) => {
export default function List(props: ListProps) {
const { colors } = useThemeColors();
const scrollRef = useRef();
const [notesListMode, notebooksListMode] = useSettingStore((state) => [
state.settings.notesListMode,
state.settings.notebooksListMode
]);
const isCompactModeEnabled =
(type === "notes" && notesListMode === "compact") ||
type === "notebooks" ||
(props.dataType === "note" && notesListMode === "compact") ||
props.dataType === "notebook" ||
notebooksListMode === "compact";
const groupType =
screen === "Home" ? "home" : screen === "Favorites" ? "favorites" : type;
props.renderedInRoute === "Notes"
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: `${props.dataType}s`;
const groupOptions = useGroupOptions(groupType);
const dateBy =
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
const renderItem = React.useCallback(
({ item, index }) => (
<RenderItem
item={item}
index={index}
color={headerProps?.color}
title={headerProps?.heading}
dateBy={dateBy}
type={groupType}
screen={screen}
isSheet={isSheet}
groupOptions={groupOptions}
/>
),
[
headerProps?.color,
headerProps?.heading,
screen,
isSheet,
dateBy,
groupType,
groupOptions
]
);
const _onRefresh = async () => {
Sync.run("global", false, true, () => {
if (refreshCallback) {
refreshCallback();
}
props.onRefresh?.();
});
};
const _onScroll = React.useCallback(
(event) => {
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}
/>
);
}
},
[
groupOptions,
groupType,
props.customAccentColor,
props.data,
props.dataType,
props.isRenderedInActionSheet,
props.renderedInRoute
]
);
const onListScroll = React.useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (!event) return;
let y = event.nativeEvent.contentOffset.y;
eSendEvent(eScrollEvent, {
y,
screen
y: event.nativeEvent.contentOffset.y,
screen: props.renderedInRoute
});
},
[screen]
[props.renderedInRoute]
);
useEffect(() => {
eSendEvent(eScrollEvent, {
y: 0,
screen
screen: props.renderedInRoute
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let styles = {
const styles = {
width: "100%",
minHeight: 1,
minWidth: 1
};
const ListView = ScrollComponent ? ScrollComponent : FlashList;
const ListView = props.CustomListComponent
? props.CustomListComponent
: FlashList;
return (
<>
<Animated.View
style={{
flex: 1
}}
entering={type === "search" ? undefined : FadeInDown}
entering={props.renderedInRoute === "Search" ? undefined : FadeInDown}
>
<ListView
{...handlers}
style={styles}
ref={scrollRef}
testID={notesnook.list.id}
data={listData}
data={props.data?.ids || []}
renderItem={renderItem}
onScroll={_onScroll}
onScroll={onListScroll}
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
tabBarRef.current?.unlock();
onMomentumScrollEnd?.();
}}
getItemType={(item) => item.itemType || item.type}
getItemType={(item: any) => (isGroupHeader(item) ? "header" : "item")}
estimatedItemSize={isCompactModeEnabled ? 60 : 100}
directionalLockEnabled={true}
keyboardShouldPersistTaps="always"
@@ -202,44 +194,31 @@ const List = ({
/>
}
ListEmptyComponent={
placeholderData ? (
props.placeholder ? (
<Empty
loading={loading}
placeholderData={placeholderData}
headerProps={headerProps}
type={type}
screen={screen}
loading={props.loading}
title={props.headerTitle}
dataType={props.dataType}
color={props.customAccentColor}
placeholder={props.placeholder}
/>
) : null
}
ListFooterComponent={<Footer />}
ListHeaderComponent={
<>
{ListHeader ? (
ListHeader
) : !headerProps ? null : (
{props.CustomLisHeader ? (
props.CustomLisHeader
) : !props.headerTitle ? null : (
<Header
title={headerProps.heading}
color={headerProps.color}
type={type}
screen={screen}
warning={warning}
color={props.customAccentColor}
screen={props.renderedInRoute}
/>
)}
</>
}
/>
</Animated.View>
{listData ? (
<JumpToSectionDialog
screen={screen}
data={listData}
type={screen === "Home" ? "home" : type}
scrollRef={scrollRef}
/>
) : null}
</>
);
};
export default List;
}

View File

@@ -0,0 +1,269 @@
/*
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 React from "react";
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
import {
GroupingKey,
Item,
VirtualizedGrouping,
Color,
Reminder,
Notebook,
Tag,
TrashItem,
ItemType,
Note
} from "@notesnook/core";
import { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { NoteWrapper } from "../list-items/note/wrapper";
import { db } from "../../common/database";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
export type WithDateEdited<T> = { items: T[]; dateEdited: number };
export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
export type TagsWithDateEdited = WithDateEdited<Tag>;
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
id: string;
isSheet: boolean;
index: number;
};
export function ListItemWrapper(props: ListItemWrapperProps) {
const { id, items, group, isSheet, index } = props;
const [item, setItem] = useState<Item>();
const tags = useRef<TagsWithDateEdited>();
const notebooks = useRef<NotebooksWithDateEdited>();
const reminder = useRef<Reminder>();
const color = useRef<Color>();
const totalNotes = useRef<number>(0);
const attachmentsCount = useRef(0);
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;
}
setItem(item);
})();
}, [id, items]);
if (!item) return <View style={{ height: 100, width: "100%" }} />;
const type = ((item as TrashItem).itemType || item.type) as ItemType;
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}
/>
);
}
case "notebook":
return (
<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} />
);
case "tag":
return (
<TagItem
item={item as Tag}
index={index}
totalNotes={totalNotes.current}
/>
);
default:
return null;
}
}
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 };
}
function getDate(item: Item, groupType?: GroupingKey): number {
return (
getSortValue(
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},
item
) || 0
);
}
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;
color?: Color;
tags?: TagsWithDateEdited;
attachmentsCount: number;
};
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 === "relation" ? 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);
} 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[relation.toId] = 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: Record<string, NoteResolvedData> = {};
for (const noteId in grouped) {
const group = grouped[noteId];
data[noteId] = {
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])),
notebooks: withDateEdited(
group.notebooks.map((id) => resolved.notebooks[id])
),
attachmentsCount:
(await db.attachments?.ofNote(noteId, "all"))?.length || 0
};
}
return data;
}
function isNoteResolvedData(data: unknown): data is NoteResolvedData {
return (
typeof data === "object" &&
!!data &&
"notebooks" in data &&
"reminder" in data &&
"color" in data &&
"tags" in data
);
}

View File

@@ -35,6 +35,11 @@ import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
/**
*
* @param {any} param0
* @returns
*/
export default function NotePreview({ session, content, note }) {
const { colors } = useThemeColors();
const editorId = ":noteHistory";

View File

@@ -23,6 +23,11 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useThemeColors } from "@notesnook/theme";
import Paragraph from "../ui/typography/paragraph";
/**
*
* @param {any} param0
* @returns
*/
export const ProTag = ({ width, size, background }) => {
const { colors } = useThemeColors();

View File

@@ -18,9 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { DefaultColors } from "@notesnook/core/dist/collections/colors";
import { Note } from "@notesnook/core/dist/types";
import { Color, ItemReference, Note } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
@@ -40,16 +40,15 @@ export const ColorTags = ({ item }: { item: Note }) => {
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
const updater = useRelationStore((state) => state.updater);
const getColorInfo = (colorCode: string) => {
const dbColor = db.colors.all.find((v) => v.colorCode === colorCode);
const getColorInfo = async (colorCode: string) => {
const dbColor = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", colorCode)])
);
let isLinked = false;
if (dbColor) {
const note = db.relations
.from(dbColor, "note")
.find((relation) => relation.to.id === item.id);
if (note) {
const hasRelation = await db.relations.from(dbColor, "note").has(item.id);
if (hasRelation) {
isLinked = true;
}
}
@@ -60,36 +59,16 @@ export const ColorTags = ({ item }: { item: Note }) => {
};
};
const changeColor = async (color: string) => {
const colorInfo = getColorInfo(DefaultColors[color]);
if (colorInfo.item) {
if (colorInfo.linked) {
await db.relations.unlink(colorInfo.item, item);
} else {
await db.relations.add(colorInfo.item, item);
}
} else {
const colorId = await db.colors.add({
title: color,
colorCode: DefaultColors[color]
});
const dbColor = db.colors.color(colorId);
if (dbColor) {
await db.relations.add(dbColor, item);
}
}
useRelationStore.getState().update();
setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
};
const _renderColor = (name: keyof typeof DefaultColors) => {
const ColorItem = ({ name }: { name: keyof typeof DefaultColors }) => {
const color = DefaultColors[name];
const colorInfo = getColorInfo(color);
const [colorInfo, setColorInfo] = useState<{
linked: boolean;
item: Color | undefined;
}>();
useEffect(() => {
getColorInfo(color).then((info) => setColorInfo(info));
}, [color]);
return (
<PressableButton
@@ -108,13 +87,40 @@ export const ColorTags = ({ item }: { item: Note }) => {
marginRight: isTablet ? 10 : undefined
}}
>
{colorInfo.linked ? (
{colorInfo?.linked ? (
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
) : null}
</PressableButton>
);
};
const changeColor = async (color: string) => {
const colorInfo = await getColorInfo(DefaultColors[color]);
if (colorInfo.item) {
if (colorInfo.linked) {
await db.relations.unlink(colorInfo.item, item);
} else {
await db.relations.add(colorInfo.item, item);
}
} else {
const colorId = await db.colors.add({
title: color,
colorCode: DefaultColors[color]
});
const dbColor = await db.colors.color(colorId);
if (dbColor) {
await db.relations.add(dbColor as unknown as ItemReference, item);
}
}
useRelationStore.getState().update();
setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
};
return (
<View
style={{
@@ -127,7 +133,9 @@ export const ColorTags = ({ item }: { item: Note }) => {
justifyContent: isTablet ? "center" : "space-between"
}}
>
{Object.keys(DefaultColors).map(_renderColor)}
{Object.keys(DefaultColors).map((name: keyof typeof DefaultColors) => {
return <ColorItem key={name} name={name} />;
})}
</View>
);
};

View File

@@ -53,7 +53,8 @@ export const DateMeta = ({ item }) => {
return keys.filter((key) => key.startsWith("date") && key !== "date");
}
const renderItem = (key) => (
const renderItem = (key) =>
!item[key] ? null : (
<View
key={key}
style={{

View File

@@ -35,6 +35,7 @@ import { Items } from "./items";
import Notebooks from "./notebooks";
import { Synced } from "./synced";
import { TagStrip, Tags } from "./tags";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
return (
@@ -151,7 +152,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
);
};
Properties.present = (item, buttons = [], isSheet) => {
Properties.present = async (item, buttons = [], isSheet) => {
if (!item) return;
let type = item?.type;
let props = [];
@@ -163,7 +164,7 @@ Properties.present = (item, buttons = [], isSheet) => {
break;
case "note":
android = Platform.OS === "android" ? ["pin-to-notifications"] : [];
props[0] = db.notes.note(item.id).data;
props[0] = await db.notes.note(item.id);
props.push([
"notebooks",
"add-reminder",
@@ -188,33 +189,34 @@ Properties.present = (item, buttons = [], isSheet) => {
]);
break;
case "notebook":
props[0] = db.notebooks.notebook(item.id).data;
props[0] = await db.notebooks.notebook(item.id);
props.push([
"edit-notebook",
"pin",
"add-shortcut",
"trash",
"default-notebook"
]);
break;
case "topic":
props[0] = db.notebooks
.notebook(item.notebookId)
.topics.topic(item.id)._topic;
props.push([
"move-notes",
"edit-topic",
"add-shortcut",
"trash",
"default-topic"
"default-notebook",
"add-notebook"
]);
break;
// case "topic":
// props[0] = db.notebooks
// .notebook(item.notebookId)
// .topics.topic(item.id)._topic;
// props.push([
// "move-notes",
// "edit-topic",
// "add-shortcut",
// "trash",
// "default-topic"
// ]);
// break;
case "tag":
props[0] = db.tags.tag(item.id);
props[0] = await db.tags.tag(item.id);
props.push(["add-shortcut", "trash", "rename-tag"]);
break;
case "reminder": {
props[0] = db.reminders.reminder(item.id);
props[0] = await db.reminders.reminder(item.id);
props.push(["edit-reminder", "trash", "disable-reminder"]);
break;
}

View File

@@ -28,6 +28,7 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { PressableButton } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
export const Items = ({ item, buttons, close }) => {
const { colors } = useThemeColors();
const dimensions = useSettingStore((state) => state.dimensions);

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import React, { useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
@@ -39,38 +39,20 @@ import { eClearEditor } from "../../utils/events";
export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors();
const notebooks = useNotebookStore((state) => state.notebooks);
function getNotebooks(item) {
async function getNotebooks(item) {
let filteredNotebooks = [];
const relations = db.relations.to(note, "notebook");
filteredNotebooks.push(
...relations.map((notebook) => ({
...notebook,
topics: []
}))
);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
const relations = await db.relations.to(note, "notebook").resolve();
for (let notebookReference of item.notebooks) {
let notebook = {
...(notebooks.find((item) => item.id === notebookReference.id) || {})
};
if (notebook.id) {
notebook.topics = notebook.topics.filter((topic) => {
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
});
const index = filteredNotebooks.findIndex(
(item) => item.id === notebook.id
);
if (index > -1) {
filteredNotebooks[index].topics = notebook.topics;
} else {
filteredNotebooks.push(notebook);
}
}
}
filteredNotebooks.push(relations);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
return filteredNotebooks;
}
const noteNotebooks = getNotebooks(note);
const [noteNotebooks, setNoteNotebooks] = useState([]);
useEffect(() => {
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
});
const navigateNotebook = (id) => {
let item = db.notebooks.notebook(id)?.data;

View File

@@ -27,6 +27,7 @@ import { sleep } from "../../utils/time";
import ManageTagsSheet from "../sheets/manage-tags";
import { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();

View File

@@ -1,70 +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 React from "react";
import { View } from "react-native";
import { TopicNotes } from "../../screens/notes/topic-notes";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
export const Topics = ({ item, close }) => {
const open = (topic) => {
close();
TopicNotes.navigate(topic, true);
};
const renderItem = (topic) => (
<Button
key={topic.id}
title={topic.title}
type="grayBg"
// buttonType={{
// text: colors.primary.accent
// }}
height={30}
onPress={() => open(topic)}
icon="bookmark-outline"
fontSize={SIZE.xs}
style={{
marginRight: 5,
paddingHorizontal: 8,
borderRadius: 100,
marginVertical: 5
}}
/>
);
return item &&
item.type === "notebook" &&
item.topics &&
item.topics.length > 0 ? (
<View
style={{
flexDirection: "row",
marginTop: 5,
width: "100%",
flexWrap: "wrap"
}}
>
{item.topics
.sort((a, b) => a.dateEdited - b.dateEdited)
.slice(0, 6)
.map(renderItem)}
</View>
) : null;
};

View File

@@ -1,560 +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 React, { createRef } from "react";
import {
Keyboard,
StyleSheet,
TextInput,
TouchableOpacity,
View
} from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import { ToastManager, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE, ph, pv } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
let refs = [];
export class AddNotebookSheet extends React.Component {
constructor(props) {
super(props);
refs = [];
this.state = {
notebook: props.notebook,
topics:
props.notebook?.topics?.map((item) => {
return item.title;
}) || [],
description: null,
titleFocused: false,
descFocused: false,
count: 0,
topicInputFocused: false,
editTopic: false,
loading: false
};
this.title = props.notebook?.title;
this.description = props.notebook?.description;
this.listRef;
this.prevItem = null;
this.prevIndex = null;
this.currentSelectedInput = null;
this.id = props.notebook?.id;
this.backPressCount = 0;
this.currentInputValue = null;
this.titleRef;
this.descriptionRef;
this.topicsToDelete = [];
this.hiddenInput = createRef();
this.topicInputRef = createRef();
this.addingTopic = false;
this.actionSheetRef = props.actionSheetRef;
}
componentWillUnmount() {
refs = [];
}
componentDidMount() {
sleep(300).then(() => {
!this.state.notebook && this.titleRef?.focus();
});
}
close = () => {
refs = [];
this.props.close(true);
};
onDelete = (index) => {
let { topics } = this.state;
let prevTopics = topics;
refs = [];
prevTopics.splice(index, 1);
let edit = this.state.notebook;
if (edit && edit.id) {
let topicToDelete = edit.topics[index];
if (topicToDelete) {
this.topicsToDelete.push(topicToDelete.id);
}
}
let nextTopics = [...prevTopics];
if (this.prevIndex === index) {
this.prevIndex = null;
this.prevItem = null;
this.currentInputValue = null;
this.topicInputRef.current?.setNativeProps({
text: null
});
}
this.setState({
topics: nextTopics
});
};
addNewNotebook = async () => {
this.setState({
loading: true
});
let { topics, notebook } = this.state;
if (!this.title || this.title?.trim().length === 0) {
ToastManager.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
this.setState({
loading: false
});
return;
}
let toEdit = null;
if (notebook) {
toEdit = db.notebooks.notebook(notebook.id).data;
}
let prevTopics = [...topics];
if (this.currentInputValue && this.currentInputValue.trim().length !== 0) {
if (this.prevItem != null) {
prevTopics[this.prevIndex] = this.currentInputValue;
} else {
prevTopics.push(this.currentInputValue);
this.currentInputValue = null;
}
}
let newNotebookId = null;
if (notebook) {
if (this.topicsToDelete?.length > 0) {
await db.notebooks
.notebook(toEdit.id)
.topics.delete(...this.topicsToDelete);
toEdit = db.notebooks.notebook(toEdit.id).data;
}
await db.notebooks.add({
title: this.title,
description: this.description,
id: notebook.id
});
let nextTopics = toEdit.topics.map((topic, index) => {
let copy = { ...topic };
copy.title = prevTopics[index];
return copy;
});
prevTopics.forEach((title, index) => {
if (!nextTopics[index]) {
nextTopics.push(title);
}
});
await db.notebooks.topics(toEdit.id).add(
...nextTopics.map((topic) => ({
title: topic
}))
);
this.close();
} else {
newNotebookId = await db.notebooks.add({
title: this.title,
description: this.description,
topics: prevTopics.map((topic) => ({
title: topic
})),
id: null
});
}
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
};
onSubmit = (forward = true) => {
this.hiddenInput.current?.focus();
let willFocus = true;
let { topics } = this.state;
if (!this.currentInputValue || this.currentInputValue?.trim().length === 0)
return;
let prevTopics = [...topics];
if (this.prevItem === null) {
prevTopics.push(this.currentInputValue);
this.setState({
topics: prevTopics
});
setTimeout(() => {
this.listRef.current?.scrollToEnd?.({ animated: true });
}, 30);
this.currentInputValue = null;
} else {
prevTopics[this.prevIndex] = this.currentInputValue;
this.setState({
topics: prevTopics
});
this.currentInputValue = null;
if (this.state.editTopic) {
this.topicInputRef.current?.blur();
Keyboard.dismiss();
this.setState({
editTopic: false
});
willFocus = false;
}
this.prevItem = null;
this.prevIndex = null;
this.currentInputValue = null;
if (forward) {
setTimeout(() => {
this.listRef.current?.scrollToEnd?.({ animated: true });
}, 30);
}
}
this.topicInputRef.current?.setNativeProps({
text: ""
});
willFocus && this.topicInputRef.current?.focus();
};
renderTopicItem = ({ item, index }) => (
<TopicItem
item={item}
onPress={(item, index) => {
this.prevIndex = index;
this.prevItem = item;
this.topicInputRef.current?.setNativeProps({
text: item
});
this.topicInputRef.current?.focus();
this.currentInputValue = item;
this.setState({
editTopic: true
});
}}
onDelete={this.onDelete}
index={index}
colors={this.props.colors}
/>
);
render() {
const { colors } = this.props;
const { topics, topicInputFocused, notebook } = this.state;
return (
<View
style={{
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
}}
>
<TextInput
ref={this.hiddenInput}
style={{
width: 1,
height: 1,
opacity: 0,
position: "absolute"
}}
blurOnSubmit={false}
/>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>
{notebook && notebook.dateCreated
? "Edit Notebook"
: "New Notebook"}
</Heading>
<Button
title="Save"
type="accent"
height={40}
style={{
borderRadius: 100,
paddingHorizontal: 24
}}
onPress={this.addNewNotebook}
/>
</View>
<Seperator />
<Input
fwdRef={(ref) => (this.titleRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
this.title = value;
}}
placeholder="Enter a title"
onSubmit={() => {
this.descriptionRef.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : null}
/>
<Input
fwdRef={(ref) => (this.descriptionRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
this.description = value;
}}
placeholder="Describe your notebook."
onSubmit={() => {
this.topicInputRef.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : null}
/>
<Input
fwdRef={this.topicInputRef}
testID={notesnook.ids.dialogs.notebook.inputs.topic}
onChangeText={(value) => {
this.currentInputValue = value;
if (this.prevItem !== null) {
refs[this.prevIndex].setNativeProps({
text: value,
style: {
borderBottomColor: colors.primary.accent
}
});
}
}}
returnKeyLabel="Done"
returnKeyType="done"
onSubmit={() => {
this.onSubmit();
}}
blurOnSubmit={false}
button={{
testID: "topic-add-button",
icon: this.state.editTopic ? "check" : "plus",
onPress: this.onSubmit,
color: topicInputFocused
? colors.primary.accent
: colors.secondary.icon
}}
placeholder="Add a topic"
/>
<FlatList
data={topics}
ref={(ref) => (this.listRef = ref)}
nestedScrollEnabled
keyExtractor={(item, index) => item + index.toString()}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
ListFooterComponent={
topics.length === 0 ? null : <View style={{ height: 50 }} />
}
renderItem={this.renderTopicItem}
/>
</View>
);
}
}
AddNotebookSheet.present = (notebook) => {
presentSheet({
component: (ref, close, _update, colors) => (
<AddNotebookSheet
actionSheetRef={ref}
notebook={notebook}
close={close}
colors={colors}
/>
)
});
};
const TopicItem = ({ item, index, colors, onPress, onDelete }) => {
const topicRef = (ref) => (refs[index] = ref);
return (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: colors.secondary.background,
borderRadius: 5,
marginVertical: 5
}}
>
<TouchableOpacity
style={{
width: "80%",
backgroundColor: "transparent",
zIndex: 10,
position: "absolute",
height: 30
}}
onPress={() => {
onPress(item, index);
}}
/>
<TextInput
ref={topicRef}
editable={false}
style={[
styles.topicInput,
{
color: colors.primary.paragraph
}
]}
defaultValue={item}
placeholderTextColor={colors.primary.placeholder}
/>
<View
style={{
width: 80,
position: "absolute",
right: 0,
alignItems: "center",
flexDirection: "row",
justifyContent: "flex-end"
}}
>
<IconButton
onPress={() => {
onPress(item, index);
}}
name="pencil"
size={SIZE.lg - 5}
color={colors.secondary.icon}
/>
<IconButton
onPress={() => {
onDelete(index);
}}
name="minus"
size={SIZE.lg}
color={colors.secondary.icon}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.3)",
justifyContent: "center",
alignItems: "center"
},
overlay: {
width: "100%",
height: "100%",
position: "absolute"
},
headingContainer: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center"
},
headingText: {
marginLeft: 5,
fontSize: SIZE.xl
},
input: {
paddingRight: 12,
paddingHorizontal: 0,
borderRadius: 0,
minHeight: 45,
fontSize: SIZE.md,
padding: pv - 2,
borderBottomWidth: 1,
marginTop: 10,
marginBottom: 5
},
addBtn: {
width: "12%",
minHeight: 45,
justifyContent: "center",
alignItems: "center",
position: "absolute",
right: 0
},
buttonContainer: {
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
width: "100%",
marginTop: 20
},
topicContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: 10
},
topicInput: {
padding: pv - 5,
fontSize: SIZE.sm,
//fontFamily: "sans-serif",
paddingHorizontal: ph,
paddingRight: 40,
paddingVertical: 10,
width: "100%",
maxWidth: "100%"
},
topicBtn: {
borderRadius: 5,
width: 40,
height: 40,
justifyContent: "center",
alignItems: "center",
position: "absolute",
right: 0
}
});

View File

@@ -0,0 +1,173 @@
/*
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 { Notebook } from "@notesnook/core";
import React, { useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
import { eOnNotebookUpdated } from "../../../utils/events";
export const AddNotebookSheet = ({
notebook,
parentNotebook,
close
}: {
notebook?: Notebook;
parentNotebook?: Notebook;
close: ((context?: string | undefined) => void) | undefined;
}) => {
const title = useRef(notebook?.title);
const description = useRef(notebook?.description);
const titleInput = useRef<TextInput>(null);
const descriptionInput = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const onSaveChanges = async () => {
if (loading) return;
setLoading(true);
if (!title.current || title?.current.trim().length === 0) {
ToastManager.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
setLoading(false);
return;
}
const id = await db.notebooks.add({
title: title.current,
description: description.current,
id: notebook?.id
});
if (parentNotebook) {
await db.relations.add(parentNotebook, {
type: "notebook",
id: id
});
}
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
eSendEvent(eOnNotebookUpdated, parentNotebook?.id);
if (notebook) {
eSendEvent(eOnNotebookUpdated, notebook.id);
}
if (!notebook) {
setTimeout(async () => {
MoveNotes.present(await db.notebooks.notebook(id));
}, 500);
} else {
close?.();
}
};
return (
<View
style={{
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>
{notebook ? "Edit Notebook" : "New Notebook"}
</Heading>
<Button
title={notebook ? "Save" : "Add"}
type="accent"
height={40}
style={{
paddingHorizontal: 24
}}
onPress={onSaveChanges}
/>
</View>
<Seperator />
<Input
fwdRef={titleInput}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
title.current = value;
}}
placeholder="Enter a title"
onSubmit={() => {
descriptionInput.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : ""}
/>
<Input
fwdRef={descriptionInput}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
description.current = value;
}}
placeholder="Describe your notebook."
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : ""}
/>
</View>
);
};
AddNotebookSheet.present = (notebook?: Notebook, parentNotebook?: Notebook) => {
presentSheet({
component: (ref, close) => (
<AddNotebookSheet
notebook={notebook}
parentNotebook={parentNotebook}
close={close}
/>
)
});
};

View File

@@ -20,10 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createContext, useContext } from "react";
export const SelectionContext = createContext({
toggleSelection: (item) => null,
deselect: (item) => null,
select: (item) => null,
deselectAll: () => null
toggleSelection: (item) => {},
deselect: (item) => {},
select: (item) => {},
deselectAll: () => {}
});
export const SelectionProvider = SelectionContext.Provider;
export const useSelectionContext = () => useContext(SelectionContext);

View File

@@ -17,38 +17,47 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useMemo } from "react";
import { Note, Notebook } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useCallback, useEffect, useMemo } from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../../services/event-manager";
import { eSendEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { eOnTopicSheetUpdate } from "../../../utils/events";
import { useThemeColors } from "@notesnook/theme";
import { eOnNotebookUpdated } from "../../../utils/events";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
import { Button } from "../../ui/button";
import Paragraph from "../../ui/typography/paragraph";
import { SelectionProvider } from "./context";
import { FilteredList } from "./filtered-list";
import { ListItem } from "./list-item";
import { useItemSelectionStore } from "./store";
import { useRelationStore } from "../../../stores/use-relation-store";
const MoveNoteSheet = ({ note, actionSheetRef }) => {
/**
* Render all notebooks
* Render sub notebooks
* fix selection, remove topics stuff.
* show already selected notebooks regardless of their level
* show intermediate selection for nested notebooks at all levels.
* @returns
*/
const MoveNoteSheet = ({
note,
actionSheetRef
}: {
note: Note;
actionSheetRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const notebooks = useNotebookStore((state) =>
state.notebooks.filter((n) => n?.type === "notebook")
);
const notebooks = useNotebookStore((state) => state.notebooks);
const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
@@ -57,90 +66,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const onAddNotebook = async (title) => {
if (!title || title.trim().length === 0) {
ToastManager.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
return false;
}
await db.notebooks.add({
title: title,
description: null,
topics: [],
id: null
});
setNotebooks();
return true;
};
const openAddTopicDialog = (item) => {
presentDialog({
context: "move_note",
input: true,
inputPlaceholder: "Enter title",
title: "New topic",
paragraph: "Add a new topic in " + item.title,
positiveText: "Add",
positivePress: (value) => {
return onAddTopic(value, item);
}
});
};
const onAddTopic = useCallback(
async (value, item) => {
if (!value || value.trim().length === 0) {
ToastManager.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return false;
}
await db.notebooks.topics(item.id).add({
title: value
});
setNotebooks();
return true;
},
[setNotebooks]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const getSelectedNotesCountInItem = React.useCallback(
(item) => {
switch (item.type) {
case "notebook": {
const notes = db.relations.from(item, "note");
if (notes.length === 0) return 0;
let count = 0;
selectedItemsList.forEach((item) =>
notes.findIndex((note) => note.id === item.id) > -1
? count++
: undefined
);
return count;
}
case "topic": {
const noteIds = db.notes?.topicReferences.get(item.id);
let count = 0;
selectedItemsList.forEach((item) =>
noteIds.indexOf(item.id) > -1 ? count++ : undefined
);
return count;
}
}
},
[selectedItemsList]
);
useEffect(() => {
resetItemState();
return () => {
useItemSelectionStore.getState().setMultiSelect(false);
useItemSelectionStore.getState().setItemState({});
@@ -148,68 +74,10 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const resetItemState = useCallback(
(state) => {
const itemState = {};
const notebooks = db.notebooks.all;
let count = 0;
for (let notebook of notebooks) {
itemState[notebook.id] = state
? state
: areAllSelectedItemsInNotebook(notebook, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[notebook.id] === "selected") {
count++;
}
for (let topic of notebook.topics) {
itemState[topic.id] = state
? state
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
getSelectedNotesCountInItem(topic, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[topic.id] === "selected") {
count++;
}
}
}
if (count > 1) {
useItemSelectionStore.getState().setMultiSelect(true);
} else {
useItemSelectionStore.getState().setMultiSelect(false);
}
useItemSelectionStore.getState().setItemState(itemState);
},
[getSelectedNotesCountInItem, selectedItemsList]
);
const getItemsForItem = (item) => {
switch (item.type) {
case "notebook":
return item.topics?.filter((t) => t.type === "topic");
}
};
function areAllSelectedItemsInNotebook(notebook, items) {
const notes = db.relations.from(notebook, "note");
if (notes.length === 0) return false;
return items.every((item) => {
return notes.find((note) => note.id === item.id);
});
}
function areAllSelectedItemsInTopic(topic, items) {
return items.every((item) => {
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
});
}
const updateItemState = useCallback(function (item, state) {
const updateItemState = useCallback(function (
item: Notebook,
state: "selected" | "intermediate" | "deselected"
) {
const itemState = { ...useItemSelectionStore.getState().itemState };
const mergeState = {
[item.id]: state
@@ -218,11 +86,12 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
...itemState,
...mergeState
});
}, []);
},
[]);
const contextValue = useMemo(
() => ({
toggleSelection: (item) => {
toggleSelection: (item: Notebook) => {
const itemState = useItemSelectionStore.getState().itemState;
if (itemState[item.id] === "selected") {
updateItemState(item, "deselected");
@@ -230,68 +99,43 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
updateItemState(item, "selected");
}
},
deselect: (item) => {
deselect: (item: Notebook) => {
updateItemState(item, "deselected");
},
select: (item) => {
select: (item: Notebook) => {
updateItemState(item, "selected");
},
deselectAll: (state) => {
resetItemState(state);
deselectAll: () => {
useItemSelectionStore.setState({
itemState: {}
});
}
}),
[resetItemState, updateItemState]
[updateItemState]
);
const getItemFromId = (id) => {
for (const nb of notebooks) {
if (nb.id === id) return nb;
for (const tp of nb.topics) {
if (tp.id === id) return tp;
}
}
};
const onSave = async () => {
const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id);
const noteIds = note
? [note.id]
: selectedItemsList.map((n) => (n as Note).id);
const itemState = useItemSelectionStore.getState().itemState;
for (const id in itemState) {
const item = getItemFromId(id);
const item = await db.notebooks.notebook(id);
if (!item) continue;
if (itemState[id] === "selected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.add(item, { id: noteId, type: "note" });
}
} else {
await db.notes.addToNotebook(
{
topic: item.id,
id: item.notebookId,
rebuildCache: true
},
...noteIds
);
}
} else if (itemState[id] === "deselected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.unlink(item, { id: noteId, type: "note" });
}
} else {
await db.notes.removeFromNotebook(
{
id: item.notebookId,
topic: item.id,
rebuildCache: true
},
...noteIds
);
}
}
}
Navigation.queueRoutesForUpdate();
setNotebooks();
eSendEvent(eOnTopicSheetUpdate);
eSendEvent(eOnNotebookUpdated);
SearchService.updateAndSearch();
useRelationStore.getState().update();
actionSheetRef.current?.hide();
@@ -360,7 +204,9 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
}}
type="grayAccent"
onPress={() => {
resetItemState();
useItemSelectionStore.setState({
itemState: {}
});
}}
/>
</View>
@@ -370,12 +216,12 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
style={{
paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85,
height: 50 * (notebooks.length + 2)
height: 50 * ((notebooks?.ids.length || 0) + 2)
}}
>
<FilteredList
ListEmptyComponent={
notebooks.length > 0 ? null : (
notebooks?.ids.length ? null : (
<View
style={{
width: "100%",
@@ -396,7 +242,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
)
}
estimatedItemSize={50}
data={notebooks}
data={notebooks?.ids.length}
hasHeaderSearch={true}
renderItem={({ item, index }) => (
<ListItem

View File

@@ -24,7 +24,11 @@ import Share from "react-native-share";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import {
presentSheet,
PresentSheetOptions,
ToastManager
} from "../../../services/event-manager";
import Exporter from "../../../services/exporter";
import PremiumService from "../../../services/premium";
import { useThemeColors } from "@notesnook/theme";
@@ -44,20 +48,35 @@ import { eSendEvent } from "../../../services/event-manager";
import { eCloseSheet } from "../../../utils/events";
import { requestInAppReview } from "../../../services/app-review";
import { Dialog } from "../../dialog";
import { Note } from "@notesnook/core";
const ExportNotesSheet = ({ notes, update }) => {
const ExportNotesSheet = ({
notes,
update
}: {
notes: Note[];
update: ((props: PresentSheetOptions) => void) | undefined;
}) => {
const { colors } = useThemeColors();
const [exporting, setExporting] = useState(false);
const [complete, setComplete] = useState(false);
const [result, setResult] = useState({});
const [result, setResult] = useState<
| {
fileName: string;
filePath: string;
name: string;
type: string;
}
| undefined
>();
const [status, setStatus] = useState(null);
const premium = useUserStore((state) => state.premium);
const save = async (type) => {
const save = async (type: "pdf" | "txt" | "md" | "html") => {
if (exporting) return;
if (!PremiumService.get() && type !== "txt") return;
setExporting(true);
update({ disableClosing: true });
update?.({ disableClosing: true } as PresentSheetOptions);
setComplete(false);
let result;
if (notes.length > 1) {
@@ -67,11 +86,11 @@ const ExportNotesSheet = ({ notes, update }) => {
await sleep(1000);
}
if (!result) {
update({ disableClosing: false });
update?.({ disableClosing: false } as PresentSheetOptions);
return setExporting(false);
}
setResult(result);
update({ disableClosing: false });
setResult(result as any);
update?.({ disableClosing: false } as PresentSheetOptions);
setComplete(true);
setExporting(false);
requestInAppReview();
@@ -267,7 +286,7 @@ const ExportNotesSheet = ({ notes, update }) => {
}}
>
Your {notes.length > 1 ? "notes are" : "note is"} exported
successfully as {result.fileName}
successfully as {result?.fileName}
</Paragraph>
<Button
title="Open"
@@ -279,9 +298,10 @@ const ExportNotesSheet = ({ notes, update }) => {
borderRadius: 100
}}
onPress={async () => {
if (!result?.filePath) return;
eSendEvent(eCloseSheet);
await sleep(500);
FileViewer.open(result.filePath, {
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
@@ -305,6 +325,7 @@ const ExportNotesSheet = ({ notes, update }) => {
borderRadius: 100
}}
onPress={async () => {
if (!result?.filePath) return;
if (Platform.OS === "ios") {
Share.open({
url: result.filePath
@@ -314,7 +335,7 @@ const ExportNotesSheet = ({ notes, update }) => {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
}).catch(console.log);
} as any).catch(console.log);
}
}}
/>
@@ -329,7 +350,7 @@ const ExportNotesSheet = ({ notes, update }) => {
}}
onPress={async () => {
setComplete(false);
setResult(null);
setResult(undefined);
setExporting(false);
}}
/>
@@ -342,13 +363,11 @@ const ExportNotesSheet = ({ notes, update }) => {
);
};
ExportNotesSheet.present = (notes, allNotes) => {
ExportNotesSheet.present = async (notes?: Note[], allNotes?: boolean) => {
const exportNotes = allNotes ? await db.notes.all?.items() : notes || [];
presentSheet({
component: (ref, close, update) => (
<ExportNotesSheet
notes={allNotes ? db.notes.all : notes}
update={update}
/>
<ExportNotesSheet notes={exportNotes} update={update} />
),
keyboardHandlerDisabled: true
});

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Tags } from "@notesnook/core/dist/collections/tags";
import { GroupedItems, Note, Tag } from "@notesnook/core/dist/types";
import { Note, Tag, isGroupHeader } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
@@ -42,9 +42,16 @@ import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { VirtualizedGrouping } from "@notesnook/core";
function ungroup(items: GroupedItems<Tag>) {
return items.filter((item) => item.type !== "header") as Tag[];
function tagHasSomeNotes(tagId: string, noteIds: string[]) {
return db.relations.from({ type: "tag", id: tagId }, "note").has(...noteIds);
}
function tagHasAllNotes(tagId: string, noteIds: string[]) {
return db.relations
.from({ type: "tag", id: tagId }, "note")
.hasAll(...noteIds);
}
const ManageTagsSheet = (props: {
@@ -53,49 +60,18 @@ const ManageTagsSheet = (props: {
}) => {
const { colors } = useThemeColors();
const notes = useMemo(() => props.notes || [], [props.notes]);
const allTags = useTagStore((state) => ungroup(state.tags));
const [tags, setTags] = useState<Tag[]>([]);
const tags = useTagStore((state) => state.tags);
const [query, setQuery] = useState<string>();
const inputRef = useRef<TextInput>(null);
const [focus, setFocus] = useState(false);
const [queryExists, setQueryExists] = useState(false);
const sortTags = useCallback(() => {
let _tags = db.tags.all;
_tags = _tags.sort((a, b) => a.title.localeCompare(b.title));
if (query) {
_tags = db.lookup.tags(_tags, query) as Tag[];
}
let tagsMerged = notes
.map((note) => db.relations.to(note, "tag").resolved())
.flat();
// Get unique tags and remove duplicates
tagsMerged = [
...new Map(tagsMerged.map((item) => [item.id, item])).values()
];
if (!tagsMerged || !tagsMerged.length) {
setTags(_tags);
return;
}
let noteTags = [];
for (const tag of tagsMerged) {
const index = _tags.findIndex((t) => t.id === tag.id);
if (index !== -1) {
noteTags.push(_tags[index]);
_tags.splice(index, 1);
}
}
noteTags = noteTags.sort((a, b) => a.title.localeCompare(b.title));
const combinedTags = [...noteTags, ..._tags];
setTags(combinedTags);
}, [notes, query]);
// useEffect(() => {
// sortTags();
// }, [allTags.length]);
const checkQueryExists = (query: string) => {
db.tags.all
.find((v) => v.and([v(`title`, "==", query)]))
.then((exists) => setQueryExists(!!exists));
};
const onSubmit = async () => {
if (!query || query === "" || query.trimStart().length == 0) {
@@ -109,29 +85,35 @@ const ManageTagsSheet = (props: {
const tag = query;
setQuery(undefined);
inputRef.current?.setNativeProps({
text: ""
});
try {
const exists = db.tags.all.filter((t: Tag) => t.title === tag);
const id = exists.length
? exists[0]?.id
const exists = await db.tags.all.find((v) =>
v.and([v(`title`, "==", tag)])
);
const id = exists
? exists?.id
: await db.tags.add({
title: tag
});
const createdTag = db.tags.tag(id);
if (createdTag) {
if (id) {
for (const note of notes) {
await db.relations.add(createdTag, note);
await db.relations.add(
{
id: id,
type: "tag"
},
note
);
}
}
useRelationStore.getState().update();
useTagStore.getState().setTags();
setTimeout(() => {
sortTags();
});
} catch (e) {
ToastManager.show({
heading: "Cannot add tag",
@@ -165,9 +147,7 @@ const ManageTagsSheet = (props: {
autoCapitalize="none"
onChangeText={(v) => {
setQuery(Tags.sanitize(v));
setTimeout(() => {
sortTags();
});
checkQueryExists(Tags.sanitize(v));
}}
onFocusInput={() => {
setFocus(true);
@@ -187,7 +167,7 @@ const ManageTagsSheet = (props: {
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
{query && query !== tags[0]?.title ? (
{query && !queryExists ? (
<PressableButton
key={"query_item"}
customStyle={{
@@ -205,7 +185,7 @@ const ManageTagsSheet = (props: {
<Icon name="plus" color={colors.selected.icon} size={SIZE.lg} />
</PressableButton>
) : null}
{!allTags || allTags.length === 0 ? (
{!tags || tags.ids.length === 0 ? (
<View
style={{
width: "100%",
@@ -226,8 +206,15 @@ const ManageTagsSheet = (props: {
</View>
) : null}
{tags.map((item) => (
<TagItem key={item.id} tag={item} notes={notes} />
{tags?.ids
.filter((id) => !isGroupHeader(id))
.map((item) => (
<TagItem
key={item as string}
tags={tags}
id={item as string}
notes={notes}
/>
))}
</ScrollView>
</View>
@@ -244,24 +231,56 @@ ManageTagsSheet.present = (notes?: Note[]) => {
export default ManageTagsSheet;
const TagItem = ({ tag, notes }: { tag: Tag; notes: Note[] }) => {
const TagItem = ({
id,
notes,
tags
}: {
id: string;
notes: Note[];
tags: VirtualizedGrouping<Tag>;
}) => {
const { colors } = useThemeColors();
const [tag, setTag] = useState<Tag>();
const [selection, setSelection] = useState({
all: false,
some: false
});
const update = useRelationStore((state) => state.updater);
const someNotesTagged = notes.some((note) => {
const relations = db.relations.from(tag, "note");
return relations.findIndex((relation) => relation.to.id === note.id) > -1;
const refresh = useCallback(() => {
tags.item(id).then(async (tag) => {
if (tag?.id) {
setSelection({
all: await tagHasAllNotes(
tag.id,
notes.map((note) => note.id)
),
some: await tagHasSomeNotes(
tag.id,
notes.map((note) => note.id)
)
});
}
setTag(tag);
});
}, [id, tags, notes]);
const allNotesTagged = notes.every((note) => {
const relations = db.relations.from(tag, "note");
return relations.findIndex((relation) => relation.to.id === note.id) > -1;
});
if (tag?.id !== id) {
refresh();
}
useEffect(() => {
if (tag?.id === id) {
refresh();
}
}, [id, refresh, tag?.id, update]);
const onPress = async () => {
for (const note of notes) {
try {
if (someNotesTagged) {
if (!tag?.id) return;
if (selection.all) {
await db.relations.unlink(tag, note);
} else {
await db.relations.add(tag, note);
@@ -275,6 +294,7 @@ const TagItem = ({ tag, notes }: { tag: Tag; notes: Note[] }) => {
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 1);
refresh();
};
return (
<PressableButton
@@ -287,6 +307,7 @@ const TagItem = ({ tag, notes }: { tag: Tag; notes: Note[] }) => {
onPress={onPress}
type="gray"
>
{!tag ? null : (
<IconButton
size={22}
customStyle={{
@@ -294,27 +315,40 @@ const TagItem = ({ tag, notes }: { tag: Tag; notes: Note[] }) => {
width: 23,
height: 23
}}
onPress={onPress}
color={
someNotesTagged || allNotesTagged
selection.some || selection.all
? colors.selected.icon
: colors.primary.icon
}
testID={
allNotesTagged
selection.all
? "check-circle-outline"
: someNotesTagged
: selection.some
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
allNotesTagged
selection.all
? "check-circle-outline"
: someNotesTagged
: selection.some
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<Paragraph size={SIZE.sm}>{"#" + tag.title}</Paragraph>
)}
{tag ? (
<Paragraph size={SIZE.sm}>{"#" + tag?.title}</Paragraph>
) : (
<View
style={{
width: 200,
height: 30,
backgroundColor: colors.secondary.background,
borderRadius: 5
}}
/>
)}
</PressableButton>
);
};

View File

@@ -17,54 +17,52 @@ 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, Topic } from "@notesnook/core/dist/types";
import { VirtualizedGrouping } from "@notesnook/core";
import { Note, Notebook } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useState } from "react";
import { Platform, useWindowDimensions, View } from "react-native";
import React, { RefObject, useEffect, useState } from "react";
import { Platform, View, useWindowDimensions } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import { db } from "../../../common/database";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../../services/event-manager";
import { presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search";
import { eCloseSheet } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
export const MoveNotes = ({
notebook,
selectedTopic,
fwdRef
}: {
notebook: Notebook;
selectedTopic?: Topic;
fwdRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const [currentNotebook, setCurrentNotebook] = useState(notebook);
const { height } = useWindowDimensions();
let notes = db.notes?.all;
const [selectedNoteIds, setSelectedNoteIds] = useState<string[]>([]);
const [topic, setTopic] = useState(selectedTopic);
notes = notes.filter((note) => {
if (!topic) return [];
const noteIds = db.notes?.topicReferences.get(topic.id);
return noteIds.indexOf(note.id) === -1;
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const [existingNoteIds, setExistingNoteIds] = useState<string[]>([]);
useEffect(() => {
db.notes?.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
setNotes(notes);
});
db.relations
.from(currentNotebook, "note")
.get()
.then((existingNotes) => {
setExistingNoteIds(
existingNotes.map((existingNote) => existingNote.toId)
);
});
}, [currentNotebook]);
const select = React.useCallback(
(id: string) => {
@@ -86,128 +84,20 @@ export const MoveNotes = ({
[selectedNoteIds]
);
const openAddTopicDialog = () => {
presentDialog({
context: "local",
input: true,
inputPlaceholder: "Enter title",
title: "New topic",
paragraph: "Add a new topic in " + currentNotebook.title,
positiveText: "Add",
positivePress: (value) => {
return addNewTopic(value as string);
}
});
};
const addNewTopic = async (value: string) => {
if (!value || value.trim().length === 0) {
ToastManager.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return false;
}
await db.notebooks?.topics(currentNotebook.id).add({
title: value
});
const notebook = db.notebooks?.notebook(currentNotebook.id);
if (notebook) {
setCurrentNotebook(notebook.data);
}
Navigation.queueRoutesForUpdate();
return true;
};
const renderItem = React.useCallback(
({ item }: { item: Topic | Note }) => {
({ item }: { item: string }) => {
return (
<PressableButton
testID="listitem.select"
onPress={() => {
if (item.type == "topic") {
setTopic(topic || item);
} else {
select(item.id);
}
}}
type={"transparent"}
customStyle={{
paddingVertical: 12,
justifyContent: "space-between",
paddingHorizontal: 12,
flexDirection: "row"
}}
>
<View
style={{
flexShrink: 1
}}
>
<Paragraph
numberOfLines={1}
color={
item?.id === topic?.id
? colors.primary.accent
: colors.primary.paragraph
}
>
{item.title}
</Paragraph>
{item.type == "note" && item.headline ? (
<Paragraph
numberOfLines={1}
color={colors.secondary.paragraph}
size={SIZE.xs}
>
{item.headline}
</Paragraph>
) : null}
</View>
{item.type === "topic" ? (
<Paragraph
style={{
fontSize: SIZE.xs
}}
color={colors.secondary.paragraph}
>
{item.notes?.length} Notes
</Paragraph>
) : null}
{selectedNoteIds.indexOf(item.id) > -1 ? (
<IconButton
customStyle={{
width: undefined,
height: undefined,
backgroundColor: "transparent"
}}
name="check"
type="selected"
color={colors.selected.icon}
<SelectableNoteItem
id={item}
items={notes}
select={select}
selected={selectedNoteIds?.indexOf(item) > -1}
/>
) : null}
</PressableButton>
);
},
[
colors.primary.accent,
colors.secondary.paragraph,
colors.primary.paragraph,
colors.selected.icon,
select,
selectedNoteIds,
topic
]
[notes, select, selectedNoteIds]
);
/**
*
*/
return (
<View
style={{
@@ -217,66 +107,12 @@ export const MoveNotes = ({
}}
>
<Dialog context="local" />
{topic ? (
<PressableButton
onPress={() => {
setTopic(undefined);
}}
customStyle={{
paddingVertical: 12,
justifyContent: "space-between",
paddingHorizontal: 12,
marginBottom: 10,
alignItems: "flex-start"
}}
type="grayBg"
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%"
}}
>
<Heading size={SIZE.md}>
Adding notes to {currentNotebook.title}
</Heading>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
marginTop: 5
}}
>
<Paragraph color={colors.selected.paragraph}>
in {topic.title}
</Paragraph>
<Paragraph
style={{
fontSize: SIZE.xs
}}
>
Tap to change
</Paragraph>
</View>
</PressableButton>
) : (
<>
<DialogHeader
title={`Add notes to ${currentNotebook.title}`}
paragraph={
"Select the topic in which you would like to move notes."
}
paragraph={"Select the topic in which you would like to move notes."}
/>
<Seperator />
</>
)}
<FlashList
ListEmptyComponent={
@@ -288,41 +124,25 @@ export const MoveNotes = ({
}}
>
<Paragraph color={colors.secondary.paragraph}>
{topic ? "No notes to show" : "No topics in this notebook"}
No notes to show
</Paragraph>
{!topic && (
<Button
style={{
marginTop: 10,
height: 40
}}
onPress={() => {
openAddTopicDialog();
}}
title="Add first topic"
type="grayAccent"
/>
)}
</View>
}
data={topic ? notes : currentNotebook.topics}
data={(notes?.ids as string[])?.filter(
(id) => existingNoteIds?.indexOf(id) === -1
)}
renderItem={renderItem}
/>
{selectedNoteIds.length > 0 ? (
<Button
onPress={async () => {
if (!topic) return;
await db.notes?.addToNotebook(
{
topic: topic.id,
id: topic.notebookId
},
currentNotebook.id,
...selectedNoteIds
);
Navigation.queueRoutesForUpdate();
SearchService.updateAndSearch();
eSendEvent(eCloseSheet);
fwdRef?.current?.hide();
}}
title="Move selected notes"
type="accent"
@@ -333,10 +153,81 @@ export const MoveNotes = ({
);
};
MoveNotes.present = (notebook: Notebook, topic: Topic) => {
const SelectableNoteItem = ({
id,
items,
select,
selected
}: {
id: string;
items?: VirtualizedGrouping<Note>;
select: (id: string) => void;
selected?: boolean;
}) => {
const { colors } = useThemeColors();
const [item, setItem] = useState<Note>();
useEffect(() => {
items?.item(id).then((item) => setItem(item));
}, [id, items]);
return !item ? null : (
<PressableButton
testID="listitem.select"
onPress={() => {
if (!item) return;
select(item?.id);
}}
type={"transparent"}
customStyle={{
paddingVertical: 12,
flexDirection: "row",
width: "100%",
justifyContent: "flex-start",
height: 50
}}
>
<IconButton
customStyle={{
backgroundColor: "transparent",
marginRight: 5
}}
onPress={() => {
if (!item) return;
select(item?.id);
}}
name={
selected ? "check-circle-outline" : "checkbox-blank-circle-outline"
}
type="selected"
color={selected ? colors.selected.icon : colors.primary.icon}
/>
<View
style={{
flexShrink: 1
}}
>
<Paragraph numberOfLines={1}>{item?.title}</Paragraph>
{item.type == "note" && item.headline ? (
<Paragraph
numberOfLines={1}
color={colors?.secondary.paragraph}
size={SIZE.xs}
>
{item.headline}
</Paragraph>
) : null}
</View>
</PressableButton>
);
};
MoveNotes.present = (notebook?: Notebook) => {
if (!notebook) return;
presentSheet({
component: (ref: RefObject<ActionSheetRef>) => (
<MoveNotes fwdRef={ref} notebook={notebook} selectedTopic={topic} />
<MoveNotes fwdRef={ref} notebook={notebook} />
)
});
};

View File

@@ -16,174 +16,127 @@ 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 { GroupHeader, Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import qclone from "qclone";
import React, {
createContext,
RefObject,
useCallback,
useContext,
useEffect,
useRef,
useState
} from "react";
import { RefreshControl, useWindowDimensions, View } from "react-native";
import { RefreshControl, View, useWindowDimensions } from "react-native";
import ActionSheet, {
ActionSheetRef,
FlatList
FlashList
} from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { IconButton } from "../../../components/ui/icon-button";
import { PressableButton } from "../../../components/ui/pressable";
import Paragraph from "../../../components/ui/typography/paragraph";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
presentSheet
} from "../../../services/event-manager";
import useNavigationStore, {
NotebookScreenParams
} from "../../../stores/use-navigation-store";
import {
eOnNewTopicAdded,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
import { normalize, SIZE } from "../../../utils/size";
import { getTotalNotes } from "@notesnook/common";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import Config from "react-native-config";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import create from "zustand";
import { notesnook } from "../../../../e2e/test.ids";
import { MMKV } from "../../../common/database/mmkv";
import { useNotebook } from "../../../hooks/use-notebook";
import NotebookScreen from "../../../screens/notebook";
import { openEditor } from "../../../screens/notes/common";
import { eSendEvent, presentSheet } from "../../../services/event-manager";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnNotebookUpdated } from "../../../utils/events";
import { deleteItems } from "../../../utils/functions";
import { findRootNotebookId } from "../../../utils/notebooks";
import { SIZE, normalize } from "../../../utils/size";
import { Properties } from "../../properties";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import Sort from "../sort";
import { GroupedItems, GroupHeader, Topic } from "@notesnook/core/dist/types";
type ConfigItem = { id: string; type: string };
class TopicSheetConfig {
class NotebookSheetConfig {
static storageKey: "$$sp";
static makeId(item: ConfigItem) {
return `${TopicSheetConfig.storageKey}:${item.type}:${item.id}`;
return `${NotebookSheetConfig.storageKey}:${item.type}:${item.id}`;
}
static get(item: ConfigItem) {
return MMKV.getInt(TopicSheetConfig.makeId(item)) || 0;
return MMKV.getInt(NotebookSheetConfig.makeId(item)) || 0;
}
static set(item: ConfigItem, index = 0) {
MMKV.setInt(TopicSheetConfig.makeId(item), index);
MMKV.setInt(NotebookSheetConfig.makeId(item), index);
}
}
export const TopicsSheet = () => {
const useNotebookExpandedStore = create<{
expanded: {
[id: string]: boolean;
};
setExpanded: (id: string) => void;
}>((set, get) => ({
expanded: {},
setExpanded(id: string) {
set({
expanded: {
...get().expanded,
[id]: !get().expanded[id]
}
});
}
}));
export const NotebookSheet = () => {
const [collapsed, setCollapsed] = useState(false);
const currentScreen = useNavigationStore((state) => state.currentScreen);
const canShow =
currentScreen.name === "Notebook" || currentScreen.name === "TopicNotes";
const [notebook, setNotebook] = useState(
canShow
? db.notebooks?.notebook(
currentScreen?.notebookId || currentScreen?.id || ""
)?.data
: null
);
const [selection, setSelection] = useState<Topic[]>([]);
const canShow = currentScreen.name === "Notebook";
const [selection, setSelection] = useState<Notebook[]>([]);
const [enabled, setEnabled] = useState(false);
const { colors } = useThemeColors("sheet");
const ref = useRef<ActionSheetRef>(null);
const isTopic = currentScreen.name === "TopicNotes";
const [topics, setTopics] = useState<GroupedItems<Topic>>(
notebook
? qclone(
groupArray(notebook.topics, db.settings.getGroupOptions("topics"))
)
: []
);
const currentItem = useRef<string>();
const { fontScale } = useWindowDimensions();
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions("topics")
);
const onRequestUpdate = React.useCallback(
(data?: NotebookScreenParams) => {
if (!canShow) return;
if (!data) data = { item: notebook } as NotebookScreenParams;
const _notebook = db.notebooks?.notebook(data.item?.id)?.data;
if (_notebook) {
setNotebook(_notebook);
setTopics(
qclone(
groupArray(_notebook.topics, db.settings.getGroupOptions("topics"))
)
);
}
},
[canShow, notebook]
);
const onUpdate = useCallback(() => {
setGroupOptions({ ...(db.settings.getGroupOptions("topics") as any) });
onRequestUpdate();
}, [onRequestUpdate]);
useEffect(() => {
eSubscribeEvent("groupOptionsUpdate", onUpdate);
return () => {
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
};
}, [onUpdate]);
useEffect(() => {
const onTopicUpdate = () => {
setTimeout(() => {
onRequestUpdate();
}, 1);
};
eSubscribeEvent(eOnTopicSheetUpdate, onTopicUpdate);
eSubscribeEvent(eOnNewTopicAdded, onRequestUpdate);
return () => {
eUnSubscribeEvent(eOnTopicSheetUpdate, onRequestUpdate);
eUnSubscribeEvent(eOnNewTopicAdded, onTopicUpdate);
};
}, [onRequestUpdate]);
const [root, setRoot] = useState<string>();
const {
onUpdate: onRequestUpdate,
notebook,
nestedNotebooks: notebooks,
nestedNotebookNotesCount: totalNotes,
groupOptions
} = useNotebook(currentScreen.name === "Notebook" ? root : undefined);
const PLACEHOLDER_DATA = {
heading: "Topics",
paragraph: "You have not added any topics yet.",
button: "Add first topic",
heading: "Notebooks",
paragraph: "You have not added any notebooks yet.",
button: "Add a notebook",
action: () => {
if (!notebook) return;
eSendEvent(eOpenAddTopicDialog, { notebookId: notebook.id });
AddNotebookSheet.present(undefined, notebook);
},
loading: "Loading notebook topics"
};
const renderTopic = ({
const renderNotebook = ({
item,
index
}: {
item: Topic | GroupHeader;
item: string | GroupHeader;
index: number;
}) =>
(item as GroupHeader).type === "header" ? null : (
<TopicItem sheetRef={ref} item={item as Topic} index={index} />
<NotebookItem
items={notebooks}
id={item as string}
index={index}
totalNotes={totalNotes}
/>
);
const selectionContext = {
selection: selection,
enabled,
setEnabled,
toggleSelection: (item: Topic) => {
toggleSelection: (item: Notebook) => {
setSelection((state) => {
const selection = [...state];
const index = selection.findIndex(
@@ -204,19 +157,18 @@ export const TopicsSheet = () => {
useEffect(() => {
if (canShow) {
setTimeout(() => {
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id;
if (currentItem.current !== id) {
setTimeout(async () => {
const id = currentScreen?.id;
const nextRoot = await findRootNotebookId(id);
setRoot(nextRoot);
if (nextRoot !== currentItem.current) {
setSelection([]);
setEnabled(false);
}
currentItem.current = id;
const notebook = db.notebooks?.notebook(id as string)?.data;
const snapPoint = isTopic
? 0
: TopicSheetConfig.get({
type: isTopic ? "topic" : "notebook",
id: currentScreen.id as string
currentItem.current = nextRoot;
const snapPoint = NotebookSheetConfig.get({
type: "notebook",
id: nextRoot as string
});
if (ref.current?.isOpen()) {
@@ -224,25 +176,14 @@ export const TopicsSheet = () => {
} else {
ref.current?.show(snapPoint);
}
if (notebook) {
onRequestUpdate({
item: notebook
} as any);
}
}, 300);
onRequestUpdate();
}, 0);
} else {
setSelection([]);
setEnabled(false);
ref.current?.hide();
}
}, [
canShow,
currentScreen?.id,
currentScreen.name,
currentScreen?.notebookId,
onRequestUpdate,
isTopic
]);
}, [canShow, currentScreen?.id, currentScreen.name, onRequestUpdate]);
return (
<ActionSheet
@@ -262,9 +203,9 @@ export const TopicsSheet = () => {
}}
onSnapIndexChange={(index) => {
setCollapsed(index === 0);
TopicSheetConfig.set(
NotebookSheetConfig.set(
{
type: isTopic ? "topic" : "notebook",
type: "notebook",
id: currentScreen.id as string
},
index
@@ -326,7 +267,7 @@ export const TopicsSheet = () => {
}}
>
<Paragraph size={SIZE.xs} color={colors.primary.icon}>
TOPICS
NOTEBOOKS
</Paragraph>
<View
style={{
@@ -367,7 +308,7 @@ export const TopicsSheet = () => {
}
onPress={() => {
presentSheet({
component: <Sort screen="TopicSheet" type="topics" />
component: <Sort screen="TopicSheet" type="notebook" />
});
}}
testID="group-topic-button"
@@ -413,23 +354,24 @@ export const TopicsSheet = () => {
</View>
</View>
<SelectionContext.Provider value={selectionContext}>
<FlatList
data={topics}
<FlashList
data={notebooks?.ids}
style={{
width: "100%"
}}
estimatedItemSize={50}
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
onRequestUpdate();
eSendEvent(eOnNotebookUpdated);
}}
colors={[colors.primary.accent]}
progressBackgroundColor={colors.primary.background}
/>
}
keyExtractor={(item) => (item as Topic).id}
renderItem={renderTopic}
keyExtractor={(item) => item as string}
renderItem={renderNotebook}
ListEmptyComponent={
<View
style={{
@@ -439,7 +381,7 @@ export const TopicsSheet = () => {
height: 200
}}
>
<Paragraph color={colors.primary.icon}>No topics</Paragraph>
<Paragraph color={colors.primary.icon}>No notebooks</Paragraph>
</View>
}
ListFooterComponent={<View style={{ height: 50 }} />}
@@ -451,58 +393,82 @@ export const TopicsSheet = () => {
};
const SelectionContext = createContext<{
selection: Topic[];
selection: Notebook[];
enabled: boolean;
setEnabled: (value: boolean) => void;
toggleSelection: (item: Topic) => void;
toggleSelection: (item: Notebook) => void;
}>({
selection: [],
enabled: false,
setEnabled: (_value: boolean) => {},
toggleSelection: (_item: Topic) => {}
toggleSelection: (_item: Notebook) => {}
});
const useSelection = () => useContext(SelectionContext);
const TopicItem = ({
item,
type NotebookParentProp = {
parent?: NotebookParentProp;
item?: Notebook;
};
const NotebookItem = ({
id,
totalNotes,
currentLevel = 0,
index,
sheetRef
parent,
items
}: {
item: Topic;
id: string;
totalNotes: (id: string) => number;
currentLevel?: number;
index: number;
sheetRef: RefObject<ActionSheetRef>;
parent?: NotebookParentProp;
items?: VirtualizedGrouping<Notebook>;
}) => {
const {
nestedNotebookNotesCount,
nestedNotebooks,
notebook: item
} = useNotebook(id, items);
const screen = useNavigationStore((state) => state.currentScreen);
const { colors } = useThemeColors("sheet");
const selection = useSelection();
const isSelected =
selection.selection.findIndex((selected) => selected.id === item.id) > -1;
const isFocused = screen.id === item.id;
const notesCount = getTotalNotes(item);
selection.selection.findIndex((selected) => selected.id === item?.id) > -1;
const isFocused = screen.id === id;
const { fontScale } = useWindowDimensions();
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);
return (
<View
style={{
paddingLeft: currentLevel > 0 && currentLevel < 6 ? 15 : undefined,
width: "100%"
}}
>
<PressableButton
type={isSelected || isFocused ? "selected" : "transparent"}
onLongPress={() => {
if (selection.enabled) return;
if (selection.enabled || !item) return;
selection.setEnabled(true);
selection.toggleSelection(item);
}}
testID={`topic-sheet-item-${index}`}
testID={`topic-sheet-item-${currentLevel}-${index}`}
onPress={() => {
if (!item) return;
if (selection.enabled) {
selection.toggleSelection(item);
return;
}
TopicNotes.navigate(item, true);
NotebookScreen.navigate(item, true);
}}
customStyle={{
justifyContent: "space-between",
width: "100%",
alignItems: "center",
flexDirection: "row",
paddingHorizontal: 12,
paddingLeft: 0,
paddingRight: 12,
borderRadius: 0
}}
>
@@ -520,6 +486,10 @@ const TopicItem = ({
left={0}
bottom={0}
right={0}
customStyle={{
width: 40,
height: 40
}}
name={
isSelected
? "check-circle-outline"
@@ -527,11 +497,47 @@ const TopicItem = ({
}
/>
) : null}
<Paragraph size={SIZE.sm}>
{item.title}{" "}
{notesCount ? (
{nestedNotebooks?.ids.length ? (
<IconButton
size={SIZE.lg}
color={isSelected ? colors.selected.icon : colors.primary.icon}
onPress={() => {
useNotebookExpandedStore.getState().setExpanded(id);
}}
top={0}
left={0}
bottom={0}
right={0}
customStyle={{
width: 40,
height: 40
}}
name={expanded ? "chevron-down" : "chevron-right"}
/>
) : (
<>
{selection?.enabled ? null : (
<View
style={{
width: 40,
height: 40
}}
/>
)}
</>
)}
<Paragraph
color={
isFocused ? colors.selected.paragraph : colors.secondary.paragraph
}
size={SIZE.sm}
>
{item?.title}{" "}
{totalNotes(id) ? (
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{notesCount}
{totalNotes(id)}
</Paragraph>
) : null}
</Paragraph>
@@ -554,5 +560,23 @@ const TopicItem = ({
size={SIZE.xl}
/>
</PressableButton>
{!expanded
? null
: nestedNotebooks?.ids.map((id, index) => (
<NotebookItem
key={id as string}
id={id as string}
index={index}
totalNotes={nestedNotebookNotesCount}
currentLevel={currentLevel + 1}
items={nestedNotebooks}
parent={{
parent: parent,
item: item
}}
/>
))}
</View>
);
};

View File

@@ -36,22 +36,27 @@ import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { requestInAppReview } from "../../../services/app-review";
import { Note } from "@notesnook/core";
const PublishNoteSheet = ({ note: item }) => {
const PublishNoteSheet = ({
note: item
}: {
note: Note;
close?: (ctx?: string) => void;
}) => {
const { colors } = useThemeColors();
const actionSheetRef = useRef();
const attachmentDownloads = useAttachmentStore((state) => state.downloading);
const downloading = attachmentDownloads[`monograph-${item.id}`];
const downloading = attachmentDownloads?.[`monograph-${item.id}`];
const [selfDestruct, setSelfDestruct] = useState(false);
const [isLocked, setIsLocked] = useState(false);
const [note, setNote] = useState(item);
const [note, setNote] = useState<Note | undefined>(item);
const [publishing, setPublishing] = useState(false);
const publishUrl =
note && `https://monogr.ph/${db.monographs.monograph(note?.id)}`;
const isPublished = note && db.monographs.isPublished(note?.id);
const pwdInput = useRef();
const passwordValue = useRef();
const pwdInput = useRef(null);
const passwordValue = useRef<string>();
const publishNote = async () => {
if (publishing) return;
@@ -59,12 +64,12 @@ const PublishNoteSheet = ({ note: item }) => {
try {
if (note?.id) {
if (isLocked && !passwordValue) return;
if (isLocked && !passwordValue.current) return;
await db.monographs.publish(note.id, {
selfDestruct: selfDestruct,
password: isLocked && passwordValue.current
password: isLocked ? passwordValue.current : undefined
});
setNote(db.notes.note(note.id)?.data);
setNote(await db.notes.note(note.id));
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
@@ -72,7 +77,7 @@ const PublishNoteSheet = ({ note: item }) => {
} catch (e) {
ToastManager.show({
heading: "Could not publish note",
message: e.message,
message: (e as Error).message,
type: "error",
context: "local"
});
@@ -81,7 +86,7 @@ const PublishNoteSheet = ({ note: item }) => {
setPublishLoading(false);
};
const setPublishLoading = (value) => {
const setPublishLoading = (value: boolean) => {
setPublishing(value);
};
@@ -91,19 +96,18 @@ const PublishNoteSheet = ({ note: item }) => {
try {
if (note?.id) {
await db.monographs.unpublish(note.id);
setNote(db.notes.note(note.id)?.data);
setNote(await db.notes.note(note.id));
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
} catch (e) {
ToastManager.show({
heading: "Could not unpublish note",
message: e.message,
message: (e as Error).message,
type: "error",
context: "local"
});
}
actionSheetRef.current?.hide();
setPublishLoading(false);
};
@@ -171,10 +175,7 @@ const PublishNoteSheet = ({ note: item }) => {
<Paragraph
onPress={async () => {
try {
await openLinkInBrowser(
publishUrl,
colors.primary.accent
);
await openLinkInBrowser(publishUrl);
} catch (e) {
console.error(e);
}
@@ -192,7 +193,7 @@ const PublishNoteSheet = ({ note: item }) => {
<IconButton
onPress={() => {
Clipboard.setString(publishUrl);
Clipboard.setString(publishUrl as string);
ToastManager.show({
heading: "Note publish url copied",
type: "success",
@@ -356,10 +357,7 @@ const PublishNoteSheet = ({ note: item }) => {
}}
onPress={async () => {
try {
await openLinkInBrowser(
"https://docs.notesnook.com/monographs/",
colors.primary.accent
);
await openLinkInBrowser("https://docs.notesnook.com/monographs/");
} catch (e) {
console.error(e);
}
@@ -371,15 +369,10 @@ const PublishNoteSheet = ({ note: item }) => {
);
};
PublishNoteSheet.present = (note) => {
PublishNoteSheet.present = (note: Note) => {
presentSheet({
component: (ref, close, update) => (
<PublishNoteSheet
actionSheetRef={ref}
close={close}
update={update}
note={note}
/>
<PublishNoteSheet close={close} note={note} />
)
});
};

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 { useThemeColors } from "@notesnook/theme";
import React, { RefObject } from "react";
import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
@@ -35,7 +35,8 @@ import SheetProvider from "../../sheet-provider";
import { Button } from "../../ui/button";
import { PressableButtonProps } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { ItemReference, ItemType } from "@notesnook/core/dist/types";
import { Item, ItemReference, ItemType } from "@notesnook/core/dist/types";
import { VirtualizedGrouping } from "@notesnook/core";
type RelationsListProps = {
actionSheetRef: RefObject<ActionSheetRef>;
@@ -73,13 +74,24 @@ export const RelationsList = ({
const updater = useRelationStore((state) => state.updater);
const { colors } = useThemeColors();
const items =
const [items, setItems] = useState<VirtualizedGrouping<Item>>();
const hasNoRelations = !items || items?.ids?.length === 0;
useEffect(() => {
db.relations?.[relationType]?.(
{ id: item?.id, type: item?.type } as ItemReference,
referenceType as ItemType
) || [];
const hasNoRelations = !items || items.length === 0;
referenceType as any
)
.selector.sorted({
sortBy: "dateEdited",
sortDirection: "desc",
groupBy: "default"
})
.then((grouped) => {
setItems(grouped);
});
}, [relationType, referenceType]);
return (
<View
@@ -119,15 +131,11 @@ export const RelationsList = ({
</View>
) : (
<List
listData={items}
ScrollComponent={FlashList}
data={items}
CustomListComponent={FlashList}
loading={false}
type={referenceType}
headerProps={null}
isSheet={true}
onMomentumScrollEnd={() => {
actionSheetRef?.current?.handleChildScrollEnd();
}}
dataType={referenceType as any}
isRenderedInActionSheet={true}
/>
)}
</View>

View File

@@ -17,23 +17,29 @@ 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 dayjs from "dayjs";
import React, { RefObject } from "react";
import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
presentSheet,
PresentSheetOptions
} from "../../../services/event-manager";
import Notifications, { Reminder } from "../../../services/notifications";
import Notifications from "../../../services/notifications";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size";
import { ItemReference } from "../../../utils/types";
import List from "../../list";
import { Button } from "../../ui/button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import {
Reminder,
ItemReference,
VirtualizedGrouping,
Note
} from "@notesnook/core";
type ReminderSheetProps = {
actionSheetRef: RefObject<ActionSheetRef>;
@@ -48,7 +54,16 @@ export default function ReminderNotify({
reminder
}: ReminderSheetProps) {
const { colors } = useThemeColors();
const references = db.relations?.to(reminder as ItemReference, "note") || [];
const [references, setReferences] = useState<VirtualizedGrouping<Note>>();
useEffect(() => {
db.relations
?.to(reminder as ItemReference, "note")
.selector.grouped(db.settings.getGroupOptions("notes"))
.then((items) => {
setReferences(items);
});
}, [reminder]);
const QuickActions = [
{
@@ -76,7 +91,7 @@ export default function ReminderNotify({
snoozeUntil: snoozeTime
});
await Notifications.scheduleNotification(
db.reminders?.reminder(reminder?.id)
await db.reminders?.reminder(reminder?.id as string)
);
close?.();
};
@@ -135,12 +150,14 @@ export default function ReminderNotify({
})}
</ScrollView>
{references.length > 0 ? (
{references?.ids && references?.ids?.length > 0 ? (
<View
style={{
width: "100%",
height:
160 * references?.length < 500 ? 160 * references?.length : 500,
160 * references?.ids?.length < 500
? 160 * references?.ids?.length
: 500,
borderTopWidth: 1,
borderTopColor: colors.secondary.background,
marginTop: 5,
@@ -157,14 +174,11 @@ export default function ReminderNotify({
REFERENCED IN
</Paragraph>
<List
listData={references}
data={references}
CustomListComponent={FlashList}
loading={false}
type="notes"
headerProps={null}
isSheet={true}
onMomentumScrollEnd={() =>
actionSheetRef.current?.handleChildScrollEnd()
}
dataType="note"
isRenderedInActionSheet={true}
/>
</View>
) : null}

View File

@@ -35,7 +35,7 @@ import DatePicker from "react-native-date-picker";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import Navigation from "../../../services/navigation";
import Notifications, { Reminder } from "../../../services/notifications";
import Notifications from "../../../services/notifications";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { useRelationStore } from "../../../stores/use-relation-store";
@@ -43,7 +43,7 @@ import { Dialog } from "../../dialog";
import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { ItemReference } from "@notesnook/core/dist/types";
import { ItemReference, Note, Reminder } from "@notesnook/core";
type ReminderSheetProps = {
actionSheetRef: RefObject<ActionSheetRef>;
@@ -113,7 +113,7 @@ export default function ReminderSheet({
>(reminder?.priority || SettingsService.get().reminderNotificationMode);
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const [repeatFrequency, setRepeatFrequency] = useState(1);
const referencedItem = reference ? db.notes?.note(reference.id)?.data : null;
const referencedItem = reference ? (reference as Note) : null;
const title = useRef<string | undefined>(
reminder?.title || referencedItem?.title
@@ -195,7 +195,7 @@ export default function ReminderSheet({
disabled: false
});
if (!reminderId) return;
const _reminder = db.reminders?.reminder(reminderId);
const _reminder = await db.reminders?.reminder(reminderId);
if (!_reminder) {
ToastManager.show({

View File

@@ -244,7 +244,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
await db.backup.import(backup, password, key);
await db.initCollections();
initialize();
ToastEvent.show({
ToastManager.show({
heading: "Backup restored successfully.",
type: "success",
context: "global"
@@ -253,7 +253,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
};
const backupError = (e) => {
ToastEvent.show({
ToastManager.show({
heading: "Restore failed",
message:
e.message ||
@@ -317,7 +317,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
initialize();
setRestoring(false);
close();
ToastEvent.show({
ToastManager.show({
heading: "Backup restored successfully.",
type: "success",
context: "global"

View File

@@ -33,7 +33,7 @@ const Sort = ({ type, screen }) => {
const isTopicSheet = screen === "TopicSheet";
const { colors } = useThemeColors();
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions(type)
db.settings.getGroupOptions(screen === "Notes" ? "home" : type + "s")
);
const updateGroupOptions = async (_groupOptions) => {
await db.settings.setGroupOptions(type, _groupOptions);

View File

@@ -31,6 +31,7 @@ import { presentDialog } from "../dialog/functions";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Color } from "@notesnook/core";
export const ColorSection = React.memo(
function ColorSection() {
@@ -44,29 +45,33 @@ export const ColorSection = React.memo(
}
}, [loading, setColorNotes]);
return colorNotes.map((item, index) => {
return <ColorItem key={item.id} item={item} index={index} />;
return colorNotes.map((item) => {
return <ColorItem key={item.id} item={item} />;
});
},
() => true
);
const ColorItem = React.memo(
function ColorItem({ item }) {
function ColorItem({ item }: { item: Color }) {
const { colors, isDark } = useThemeColors();
const setColorNotes = useMenuStore((state) => state.setColorNotes);
const [headerTextState, setHeaderTextState] = useState(null);
const [headerTextState, setHeaderTextState] = useState<{
id: string | undefined;
}>({
id: undefined
});
const isFocused = headerTextState?.id === item.id;
const onHeaderStateChange = useCallback(
(state) => {
(state: any) => {
setTimeout(() => {
let id = state.currentScreen?.id;
if (id === item.id) {
setHeaderTextState({ id: state.currentScreen.id });
} else {
if (headerTextState !== null) {
setHeaderTextState(null);
setHeaderTextState({ id: undefined });
}
}
}, 300);
@@ -75,13 +80,13 @@ const ColorItem = React.memo(
);
useEffect(() => {
let unsub = useNavigationStore.subscribe(onHeaderStateChange);
const remove = useNavigationStore.subscribe(onHeaderStateChange);
return () => {
unsub();
remove();
};
}, [headerTextState, onHeaderStateChange]);
const onPress = (item) => {
const onPress = (item: Color) => {
ColoredNotes.navigate(item, false);
setImmediate(() => {

View File

@@ -39,6 +39,7 @@ import SheetWrapper from "../ui/sheet";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
import { Notebook, Tag } from "@notesnook/core";
export const TagsSection = React.memo(
function TagsSection() {
@@ -52,20 +53,18 @@ export const TagsSection = React.memo(
}
}, [loading, setMenuPins]);
const onPress = (item) => {
const onPress = (item: Notebook | Tag) => {
if (item.type === "notebook") {
NotebookScreen.navigate(item);
} else if (item.type === "tag") {
TaggedNotes.navigate(item);
} else {
TopicNotes.navigate(item);
}
setImmediate(() => {
Navigation.closeDrawer();
});
};
const renderItem = ({ item, index }) => {
return <PinItem item={item} index={index} onPress={onPress} />;
const renderItem = ({ item }: { item: Notebook | Tag; index: number }) => {
return <PinItem item={item} onPress={onPress} />;
};
return (
@@ -99,12 +98,22 @@ export const TagsSection = React.memo(
);
export const PinItem = React.memo(
function PinItem({ item, onPress, placeholder }) {
function PinItem({
item,
onPress,
isPlaceholder
}: {
item: Notebook | Tag;
onPress: (item: Notebook | Tag) => void;
isPlaceholder?: boolean;
}) {
const { colors } = useThemeColors();
const setMenuPins = useMenuStore((state) => state.setMenuPins);
const [visible, setVisible] = useState(false);
const [headerTextState, setHeaderTextState] = useState(null);
const [headerTextState, setHeaderTextState] = useState<{
id?: string;
}>({});
const primaryColors =
headerTextState?.id === item.id ? colors.selected : colors.primary;
@@ -116,16 +125,16 @@ export const PinItem = React.memo(
const fwdRef = useRef();
const onHeaderStateChange = useCallback(
(state) => {
(state: any) => {
setTimeout(() => {
let id = state.currentScreen?.id;
const id = state.currentScreen?.id;
if (id === item.id) {
setHeaderTextState({
id: state.currentScreen.id
id
});
} else {
if (headerTextState !== null) {
setHeaderTextState(null);
setHeaderTextState({});
}
}
}, 300);
@@ -134,9 +143,9 @@ export const PinItem = React.memo(
);
useEffect(() => {
let unsub = useNavigationStore.subscribe(onHeaderStateChange);
const remove = useNavigationStore.subscribe(onHeaderStateChange);
return () => {
unsub();
remove();
};
}, [headerTextState, onHeaderStateChange]);
@@ -155,7 +164,6 @@ export const PinItem = React.memo(
}}
gestureEnabled={false}
fwdRef={fwdRef}
visible={true}
>
<Seperator />
<Button
@@ -177,7 +185,7 @@ export const PinItem = React.memo(
<PressableButton
type={isFocused ? "selected" : "gray"}
onLongPress={() => {
if (placeholder) return;
if (isPlaceholder) return;
Properties.present(item);
}}
onPress={() => onPress(item)}

View File

@@ -28,6 +28,7 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
export const Tip = ({
tip,
style,
@@ -39,7 +40,7 @@ export const Tip = ({
tip: TTip;
style?: ViewStyle;
textStyle?: TextStyle;
neverShowAgain: boolean;
neverShowAgain?: boolean;
noImage?: boolean;
color?: string;
}) => {
@@ -77,9 +78,10 @@ export const Tip = ({
alignSelf: "flex-start",
borderRadius: 100,
borderWidth: 1,
borderColor:
colors.static[color as never] ||
(colors.primary[color as never] as string)
borderColor: color ? color : colors.primary.accent
}}
buttonType={{
text: color
}}
/>

View File

@@ -29,6 +29,11 @@ import { useAppState } from "../../../hooks/use-app-state";
import SettingsService from "../../../services/settings";
import { useUserStore } from "../../../stores/use-user-store";
/**
*
* @param {any} param0
* @returns
*/
const SheetWrapper = ({
children,
fwdRef,

View File

@@ -24,7 +24,6 @@ import {
Notebook,
Reminder,
Tag,
Topic,
TrashItem
} from "@notesnook/core/dist/types";
import { DisplayedNotification } from "@notifee/react-native";
@@ -39,7 +38,6 @@ import NoteHistory from "../components/note-history";
import { AddNotebookSheet } from "../components/sheets/add-notebook";
import MoveNoteSheet from "../components/sheets/add-to";
import ExportNotesSheet from "../components/sheets/export-notes";
import { MoveNotes } from "../components/sheets/move-notes/movenote";
import PublishNoteSheet from "../components/sheets/publish-note";
import { RelationsList } from "../components/sheets/relations-list/index";
import ReminderSheet from "../components/sheets/reminder";
@@ -60,11 +58,7 @@ import { useSelectionStore } from "../stores/use-selection-store";
import { useTagStore } from "../stores/use-tag-store";
import { useUserStore } from "../stores/use-user-store";
import Errors from "../utils/errors";
import {
eOnTopicSheetUpdate,
eOpenAddTopicDialog,
eOpenLoginDialog
} from "../utils/events";
import { eOpenLoginDialog } from "../utils/events";
import { deleteItems } from "../utils/functions";
import { convertNoteToText } from "../utils/note-to-text";
import { sleep } from "../utils/time";
@@ -73,7 +67,7 @@ export const useActions = ({
close,
item
}: {
item: Note | Notebook | Topic | Reminder | Tag | Color | TrashItem;
item: Note | Notebook | Reminder | Tag | Color | TrashItem;
close: () => void;
}) => {
const clearSelection = useSelectionStore((state) => state.clearSelection);
@@ -89,6 +83,7 @@ export const useActions = ({
const [defaultNotebook, setDefaultNotebook] = useState(
db.settings.getDefaultNotebook()
);
const [noteInCurrentNotebook, setNoteInCurrentNotebook] = useState(false);
const isPublished =
item.type === "note" && db.monographs.isPublished(item.id);
@@ -156,63 +151,21 @@ export const useActions = ({
}
const checkItemSynced = () => {
if (!user) return true;
if (item.type !== "note" || (item as unknown as TrashItem)) return true;
let isTrash = (item as unknown as TrashItem).type === "trash";
if (!isTrash && !db.notes.note(item.id)?.synced()) {
ToastManager.show({
context: "local",
heading: "Note not synced",
message: "Please run sync before making changes",
type: "error"
});
return false;
}
if (isTrash && !db.trash.synced(item.id)) {
ToastManager.show({
context: "local",
heading: "Note not synced",
message: "Please run sync before making changes",
type: "error"
});
return false;
}
return true;
};
async function createMenuShortcut() {
if (
item.type !== "notebook" &&
item.type !== "topic" &&
item.type !== "tag"
)
return;
if (item.type !== "notebook" && item.type !== "tag") return;
close();
try {
if (isPinnedToMenu) {
await db.shortcuts.remove(item.id);
} else {
if (item.type === "topic") {
await db.shortcuts.add({
item: {
type: "topic",
id: item.id,
notebookId: item.notebookId
}
itemId: item.id,
itemType: item.type
});
} else {
await db.shortcuts.add({
item: {
type: item.type,
id: item.id
}
});
}
}
setIsPinnedToMenu(db.shortcuts.exists(item.id));
setMenuPins();
@@ -300,23 +253,6 @@ export const useActions = ({
}
}
}
async function removeNoteFromTopic() {
if (item.type !== "note") return;
const currentScreen = useNavigationStore.getState().currentScreen;
if (currentScreen.name !== "TopicNotes" || !currentScreen.notebookId)
return;
await db.notes.removeFromNotebook(
{
id: currentScreen.notebookId,
topic: currentScreen.id
},
item.id
);
Navigation.queueRoutesForUpdate();
eSendEvent(eOnTopicSheetUpdate);
close();
}
async function deleteTrashItem() {
if (item.type !== "trash") return;
@@ -440,11 +376,7 @@ export const useActions = ({
);
}
if (
item.type === "tag" ||
item.type === "topic" ||
item.type === "notebook"
) {
if (item.type === "tag" || item.type === "notebook") {
actions.push({
id: "add-shortcut",
title: isPinnedToMenu ? "Remove Shortcut" : "Add Shortcut",
@@ -460,27 +392,12 @@ export const useActions = ({
if (item.type === "notebook") {
actions.push(
{
id: "default-notebook",
title:
defaultNotebook?.id === item.id
? "Remove as default"
: "Set as default",
hidden: item.type !== "notebook",
icon: "notebook",
id: "add-notebook",
title: "Add notebook",
icon: "plus",
func: async () => {
if (defaultNotebook?.id === item.id) {
await db.settings.setDefaultNotebook(undefined);
setDefaultNotebook(undefined);
} else {
const notebook = {
id: item.id
};
await db.settings.setDefaultNotebook(notebook);
setDefaultNotebook(notebook);
AddNotebookSheet.present(undefined, item);
}
close();
},
on: defaultNotebook?.topic ? false : defaultNotebook?.id === item.id
},
{
id: "edit-notebook",
@@ -489,61 +406,27 @@ export const useActions = ({
func: async () => {
AddNotebookSheet.present(item);
}
}
);
}
if (item.type === "topic") {
actions.push(
{
id: "edit-topic",
title: "Edit topic",
icon: "square-edit-outline",
func: async () => {
close();
await sleep(300);
eSendEvent(eOpenAddTopicDialog, {
notebookId: item.notebookId,
toEdit: item
});
}
},
{
id: "move-notes",
title: "Add notes",
icon: "plus",
func: async () => {
const notebook = db.notebooks.notebook(
(item as Topic).notebookId
)?.data;
if (notebook) {
MoveNotes.present(notebook, item as Topic);
}
}
},
{
id: "default-topic",
id: "default-notebook",
title:
defaultNotebook?.id === item.id
? "Remove as default"
: "Set as default",
hidden: item.type !== "topic",
icon: "bookmark",
defaultNotebook === item.id ? "Remove as default" : "Set as default",
hidden: item.type !== "notebook",
icon: "notebook",
func: async () => {
if (defaultNotebook?.topic === item.id) {
if (defaultNotebook === item.id) {
await db.settings.setDefaultNotebook(undefined);
setDefaultNotebook(undefined);
} else {
const notebook = {
id: item.notebookId,
topic: item.id
id: item.id
};
await db.settings.setDefaultNotebook(notebook);
setDefaultNotebook(notebook);
await db.settings.setDefaultNotebook(notebook.id);
setDefaultNotebook(notebook.id);
}
close();
},
on: defaultNotebook?.topic === item.id
on: defaultNotebook === item.id
}
);
}
@@ -579,17 +462,17 @@ export const useActions = ({
async function toggleLocalOnly() {
if (!checkItemSynced() || !user) return;
db.notes.note(item.id)?.localOnly();
await db.notes.localOnly(!(item as Note).localOnly, item?.id);
Navigation.queueRoutesForUpdate();
close();
}
const toggleReadyOnlyMode = async () => {
await db.notes.note(item.id)?.readonly();
const current = db.notes.note(item.id)?.data.readonly;
const currentReadOnly = (item as Note).localOnly;
await db.notes.readonly(!currentReadOnly, item?.id);
if (useEditorStore.getState().currentEditingNote === item.id) {
useEditorStore.getState().setReadonly(!!current);
useEditorStore.getState().setReadonly(!currentReadOnly);
}
Navigation.queueRoutesForUpdate();
close();
@@ -613,23 +496,6 @@ export const useActions = ({
close();
}
const isNoteInTopic = () => {
const currentScreen = useNavigationStore.getState().currentScreen;
if (item.type !== "note" || currentScreen.name !== "TopicNotes") return;
return (
db.notes?.topicReferences?.get(currentScreen.id)?.indexOf(item.id) > -1
);
};
const isNoteInNotebook = () => {
const currentScreen = useNavigationStore.getState().currentScreen;
if (item.type !== "note" || currentScreen.name !== "Notebook") return;
return !!db.relations
.to(item, "notebook")
.find((notebook) => notebook.id === currentScreen.id);
};
function addTo() {
clearSelection();
setSelectedItem(item);
@@ -637,9 +503,9 @@ export const useActions = ({
}
async function addToFavorites() {
if (!item.id) return;
if (!item.id || item.type !== "note") return;
close();
await db.notes.note(item.id)?.favorite();
await db.notes.favorite(item.favorite, item.id);
Navigation.queueRoutesForUpdate();
}
@@ -717,7 +583,7 @@ export const useActions = ({
});
return;
}
PublishNoteSheet.present(item);
PublishNoteSheet.present(item as Note);
}
async function shareNote() {
@@ -778,7 +644,7 @@ export const useActions = ({
}
try {
await db.vault.add(item.id);
const note = db.notes.note(item.id)?.data;
const note = await db.notes.note(item.id);
if (note?.locked) {
close();
Navigation.queueRoutesForUpdate();
@@ -862,7 +728,7 @@ export const useActions = ({
{
id: "remove-from-notebook",
title: "Remove from notebook",
hidden: !isNoteInNotebook(),
hidden: noteInCurrentNotebook,
icon: "minus-circle-outline",
func: removeNoteFromNotebook
},
@@ -879,13 +745,6 @@ export const useActions = ({
func: openHistory
},
{
id: "remove-from-topic",
title: "Remove from topic",
hidden: !isNoteInTopic(),
icon: "minus-circle-outline",
func: removeNoteFromTopic
},
{
id: "reminders",
title: "Reminders",
@@ -997,5 +856,16 @@ export const useActions = ({
);
}
useEffect(() => {
const currentScreen = useNavigationStore.getState().currentScreen;
if (item.type !== "note" || currentScreen.name !== "Notebook") return;
!!db.relations
.to(item, "notebook")
.selector.find((v) => v("id", "==", currentScreen.id))
.then((notebook) => {
setNoteInCurrentNotebook(!!notebook);
});
}, []);
return actions;
};

View File

@@ -18,11 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
AttachmentsProgressEvent,
EV,
EVENTS,
SYNC_CHECK_IDS,
SyncProgressEvent,
SyncStatusEvent
} from "@notesnook/core/dist/common";
import notifee from "@notifee/react-native";
@@ -129,19 +127,19 @@ const onFileEncryptionProgress = ({
.setEncryptionProgress(Math.round(progress / total));
};
const onDownloadingAttachmentProgress = (data) => {
const onDownloadingAttachmentProgress = (data: any) => {
useAttachmentStore.getState().setDownloading(data);
};
const onUploadingAttachmentProgress = (data) => {
const onUploadingAttachmentProgress = (data: any) => {
useAttachmentStore.getState().setUploading(data);
};
const onDownloadedAttachmentProgress = (data) => {
const onDownloadedAttachmentProgress = (data: any) => {
useAttachmentStore.getState().setDownloading(data);
};
const onUploadedAttachmentProgress = (data) => {
const onUploadedAttachmentProgress = (data: any) => {
useAttachmentStore.getState().setUploading(data);
};
@@ -234,7 +232,7 @@ async function checkForShareExtensionLaunchedInBackground() {
if (notesAddedFromIntent || shareExtensionOpened) {
const id = useEditorStore.getState().currentEditingNote;
const note = id && db.notes.note(id)?.data;
const note = id && (await db.notes.note(id));
eSendEvent("webview_reset");
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
MMKV.removeItem("shareExtensionOpened");
@@ -247,7 +245,7 @@ async function checkForShareExtensionLaunchedInBackground() {
async function saveEditorState() {
if (editorState().currentlyEditing) {
const id = useEditorStore.getState().currentEditingNote;
const note = id ? db.notes.note(id)?.data : undefined;
const note = id ? await db.notes.note(id) : undefined;
if (note?.locked) return;
const state = JSON.stringify({
@@ -514,7 +512,7 @@ export const useAppEvents = () => {
}
} else {
const id = useEditorStore.getState().currentEditingNote;
const note = id ? db.notes.note(id)?.data : undefined;
const note = id ? await db.notes.note(id) : undefined;
if (
note?.locked &&
SettingsService.get().appLockMode === "background"

View File

@@ -29,7 +29,10 @@ type AttachmentProgress = {
export const useAttachmentProgress = (
attachment: any,
encryption?: boolean
) => {
): [
AttachmentProgress | undefined,
(progress?: AttachmentProgress) => void
] => {
const progress = useAttachmentStore((state) => state.progress);
const [currentProgress, setCurrentProgress] = useState<
AttachmentProgress | undefined

View File

@@ -0,0 +1,131 @@
/*
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 {
Attachment,
Color,
Note,
Notebook,
Reminder,
Shortcut,
Tag,
VirtualizedGrouping
} from "@notesnook/core";
import React, { useEffect, useState } from "react";
import { db } from "../common/database";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../services/event-manager";
import { eDBItemUpdate } from "../utils/events";
type ItemTypeKey = {
note: Note;
notebook: Notebook;
tag: Tag;
color: Color;
reminder: Reminder;
attachment: Attachment;
shortcut: Shortcut;
};
export const useDBItem = <T extends keyof ItemTypeKey>(
id?: string,
type?: T,
items?: VirtualizedGrouping<ItemTypeKey[T]>
): [ItemTypeKey[T] | undefined, () => void] => {
const [item, setItem] = useState<ItemTypeKey[T]>();
useEffect(() => {
const onUpdateItem = (itemId?: string) => {
if (typeof itemId === "string" && itemId !== id) return;
if (!id) {
setItem(undefined);
return;
}
if (items) {
items.item(id).then((item) => {
setItem(item);
});
} else {
if (!(db as any)[type + "s"][type]) {
console.warn(
"no method found for",
`db.${type}s.${type}(id: string)`
);
} else {
(db as any)[type + "s"]
?.[type]?.(id)
.then((item: ItemTypeKey[T]) => setItem(item));
}
}
};
onUpdateItem();
eSubscribeEvent(eDBItemUpdate, onUpdateItem);
return () => {
eUnSubscribeEvent(eDBItemUpdate, onUpdateItem);
};
}, [id, type]);
return [
item as ItemTypeKey[T],
() => {
if (id) {
eSendEvent(eDBItemUpdate, id);
}
}
];
};
export const useTotalNotes = (
ids: string[],
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"
);
}
setTotalNotesById(totalNotesById);
});
}, [ids, type]);
useEffect(() => {
getTotalNotes();
}, [ids, type, getTotalNotes]);
return {
totalNotes: (id: string) => {
return totalNotesById[id] || 0;
},
getTotalNotes: getTotalNotes
};
};

View File

@@ -16,19 +16,18 @@ 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 { ItemType } from "@notesnook/core";
import { useSettingStore } from "../stores/use-setting-store";
export function useIsCompactModeEnabled(item: any) {
export function useIsCompactModeEnabled(dataType: ItemType) {
const [notebooksListMode, notesListMode] = useSettingStore((state) => [
state.settings.notebooksListMode,
state.settings.notesListMode
]);
const type = item.itemType || item.type;
if (dataType !== "note" && dataType !== "notebook") return false;
if (type !== "note" && type !== "notebook") return false;
const listMode = type === "notebook" ? notebooksListMode : notesListMode;
const listMode = dataType === "notebook" ? notebooksListMode : notesListMode;
return listMode === "compact";
}

View File

@@ -0,0 +1,87 @@
/*
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 { Notebook, VirtualizedGrouping } from "@notesnook/core";
import React, { useCallback, useEffect, useState } from "react";
import { db } from "../common/database";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { eOnNotebookUpdated } from "../utils/events";
import { useDBItem, useTotalNotes } from "./use-db-item";
export const useNotebook = (
id?: string,
items?: VirtualizedGrouping<Notebook>
) => {
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 onRequestUpdate = React.useCallback(() => {
if (!item || !id) {
console.log("unset notebook");
setNotebooks(undefined);
return;
}
db.relations
.from(item, "notebook")
.selector.sorted(db.settings.getGroupOptions("notebooks"))
.then((notebooks) => {
setNotebooks(notebooks);
});
}, [item, id]);
useEffect(() => {
onRequestUpdate();
}, [item, onRequestUpdate]);
const onUpdate = useCallback(() => {
setGroupOptions({ ...(db.settings.getGroupOptions("notebooks") as any) });
onRequestUpdate();
}, [onRequestUpdate]);
useEffect(() => {
const onNotebookUpdate = (id?: string) => {
if (typeof id === "string" && id !== id) return;
setImmediate(() => {
onRequestUpdate();
refresh();
});
};
eSubscribeEvent("groupOptionsUpdate", onUpdate);
eSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate);
return () => {
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
eUnSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate);
};
}, [onUpdate, onRequestUpdate, id]);
return {
notebook: item,
nestedNotebookNotesCount,
nestedNotebooks: notebooks,
onUpdate: onRequestUpdate,
groupOptions
};
};

View File

@@ -25,7 +25,7 @@ import { SafeAreaView } from "react-native";
import Container from "../components/container";
import DelayLayout from "../components/delay-layout";
import Intro from "../components/intro";
import { TopicsSheet } from "../components/sheets/topic-sheet";
import { NotebookSheet } from "../components/sheets/notebook-sheet";
import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
import { hideAllTooltips } from "../hooks/use-tooltip";
import Favorites from "../screens/favorites";
@@ -197,7 +197,7 @@ const _NavigationStack = () => {
<NavigationContainer onStateChange={onStateChange} ref={rootNavigatorRef}>
<Tabs />
</NavigationContainer>
{loading ? null : <TopicsSheet />}
{loading ? null : <NotebookSheet />}
</Container>
);
};

View File

@@ -98,7 +98,6 @@ export const useEditor = (
const lock = useRef(false);
const lockedSessionId = useRef<string>();
const loadingState = useRef<string>();
const postMessage = useCallback(
async <T>(type: string, data: T, waitFor = 300) =>
await post(editorRef, sessionIdRef.current, type, data, waitFor),
@@ -190,13 +189,13 @@ export const useEditor = (
)
return;
try {
if (id && !db.notes?.note(id)) {
if (id && !(await db.notes?.note(id))) {
isDefaultEditor &&
useEditorStore.getState().setCurrentlyEditingNote(null);
await reset();
return;
}
let note = id ? db.notes?.note(id)?.data : undefined;
let note = id ? await db.notes?.note(id) : undefined;
const locked = note?.locked;
if (note?.conflicted) return;
@@ -233,13 +232,12 @@ export const useEditor = (
if (!locked) {
id = await db.notes?.add(noteData);
if (!note && id) {
currentNote.current = db.notes?.note(id)?.data;
currentNote.current = await db.notes?.note(id);
const defaultNotebook = db.settings.getDefaultNotebook();
if (!state.current.onNoteCreated && defaultNotebook) {
onNoteCreated(id, {
type: defaultNotebook.topic ? "topic" : "notebook",
id: defaultNotebook.id,
notebook: defaultNotebook.topic
type: "notebook",
id: defaultNotebook
});
} else {
state.current?.onNoteCreated && state.current.onNoteCreated(id);
@@ -274,7 +272,7 @@ export const useEditor = (
await db.vault?.save(noteData as any);
}
if (id && sessionIdRef.current === currentSessionId) {
note = db.notes?.note(id)?.data as Note;
note = (await db.notes?.note(id)) as Note;
await commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved"
@@ -316,7 +314,7 @@ export const useEditor = (
noteId: currentNote.current?.id as string
};
} else if (note.contentId) {
const rawContent = await db.content?.raw(note.contentId);
const rawContent = await db.content?.get(note.contentId);
if (
rawContent &&
!isDeleted(rawContent) &&
@@ -396,7 +394,10 @@ export const useEditor = (
sessionHistoryId.current = Date.now();
await commands.setSessionId(nextSessionId);
currentNote.current = item;
await commands.setStatus(getFormattedDate(item.dateEdited), "Saved");
await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"),
"Saved"
);
await postMessage(EditorEvents.title, item.title);
loadingState.current = currentContent.current?.data;
@@ -443,7 +444,7 @@ export const useEditor = (
const isContentEncrypted =
typeof (data as ContentItem)?.data === "object";
const note = db.notes?.note(currentNote.current?.id)?.data;
const note = await db.notes?.note(currentNote.current?.id);
if (lastContentChangeTime.current >= (data as Note).dateEdited) return;

View File

@@ -37,13 +37,6 @@ const prepareSearch = () => {
});
};
const PLACEHOLDER_DATA = {
heading: "Your favorites",
paragraph: "You have not added any notes to favorites yet.",
button: null,
loading: "Loading your favorites"
};
export const Favorites = ({
navigation,
route
@@ -70,17 +63,19 @@ export const Favorites = ({
return (
<DelayLayout wait={loading}>
<List
listData={favorites}
type="notes"
refreshCallback={() => {
data={favorites}
dataType="note"
onRefresh={() => {
setFavorites();
}}
screen="Favorites"
renderedInRoute="Favorites"
loading={loading || !isFocused}
placeholderData={PLACEHOLDER_DATA}
headerProps={{
heading: "Favorites"
placeholder={{
title: "Your favorites",
paragraph: "You have not added any notes to favorites yet.",
loading: "Loading your favorites"
}}
headerTitle="Favorites"
/>
</DelayLayout>
);

View File

@@ -39,14 +39,6 @@ const prepareSearch = () => {
});
};
const PLACEHOLDER_DATA = {
heading: "Notes",
paragraph: "You have not added any notes yet.",
button: "Add your first note",
action: openEditor,
loading: "Loading your notes"
};
export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
const notes = useNoteStore((state) => state.notes);
const loading = useNoteStore((state) => state.loading);
@@ -66,19 +58,23 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
onBlur: () => false,
delay: SettingsService.get().homepage === route.name ? 1 : -1
});
return (
<DelayLayout wait={loading} delay={500}>
<List
listData={notes}
type="notes"
screen="Home"
data={notes}
dataType="note"
renderedInRoute="Notes"
loading={loading || !isFocused}
headerProps={{
heading: "Notes"
headerTitle="Notes"
placeholder={{
title: "Notes",
paragraph: "You have not added any notes yet.",
button: "Add your first note",
action: openEditor,
loading: "Loading your notes"
}}
placeholderData={PLACEHOLDER_DATA}
/>
<FloatingButton title="Create a new note" onPress={openEditor} />
</DelayLayout>
);

View File

@@ -16,8 +16,8 @@ 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, Topic } from "@notesnook/core/dist/types";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import { VirtualizedGrouping } from "@notesnook/core";
import { Note, Notebook } from "@notesnook/core/dist/types";
import React, { useEffect, useRef, useState } from "react";
import { db } from "../../common/database";
import DelayLayout from "../../components/delay-layout";
@@ -38,13 +38,9 @@ import { eOnNewTopicAdded } from "../../utils/events";
import { openEditor, setOnFirstSave } from "../notes/common";
const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const [notes, setNotes] = useState(
groupArray(
db.relations?.from(route.params.item, "note").resolved(),
db.settings.getGroupOptions("notes")
)
);
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const params = useRef<NotebookScreenParams>(route?.params);
const [loading, setLoading] = useState(true);
useNavigationFocus(navigation, {
onFocus: () => {
@@ -77,21 +73,23 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
}, [route.name]);
const onRequestUpdate = React.useCallback(
(data?: NotebookScreenParams) => {
async (data?: NotebookScreenParams) => {
if (data) params.current = data;
params.current.title = params.current.item.title;
try {
const notebook = db.notebooks?.notebook(
const notebook = await db.notebooks?.notebook(
params?.current?.item?.id
)?.data;
);
if (notebook) {
params.current.item = notebook;
const notes = db.relations?.from(notebook, "note").resolved();
setNotes(
groupArray(notes || [], db.settings.getGroupOptions("notes"))
await db.relations
.from(notebook, "note")
.selector.grouped(db.settings.getGroupOptions("notes"))
);
syncWithNavigation();
}
setLoading(false);
} catch (e) {
console.error(e);
}
@@ -100,6 +98,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
);
useEffect(() => {
onRequestUpdate();
eSubscribeEvent(eOnNewTopicAdded, onRequestUpdate);
return () => {
eUnSubscribeEvent(eOnNewTopicAdded, onRequestUpdate);
@@ -113,37 +112,35 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
}, []);
const prepareSearch = () => {
SearchService.update({
placeholder: `Search in "${params.current.title}"`,
type: "notes",
title: params.current.title,
get: () => {
const notebook = db.notebooks?.notebook(
params?.current?.item?.id
)?.data;
if (!notebook) return [];
const notes = db.relations?.from(notebook, "note") || [];
const topicNotes = db.notebooks
.notebook(notebook.id)
?.topics.all.map((topic: Topic) => {
return db.notes?.topicReferences
.get(topic.id)
.map((id: string) => db.notes?.note(id)?.data);
})
.flat()
.filter(
(topicNote) =>
notes.findIndex((note) => note?.id !== topicNote?.id) === -1
) as Note[];
return [...notes, ...topicNotes];
}
});
// SearchService.update({
// placeholder: `Search in "${params.current.title}"`,
// type: "notes",
// title: params.current.title,
// get: () => {
// const notebook = db.notebooks?.notebook(
// params?.current?.item?.id
// )?.data;
// if (!notebook) return [];
// const notes = db.relations?.from(notebook, "note") || [];
// const topicNotes = db.notebooks
// .notebook(notebook.id)
// ?.topics.all.map((topic: Topic) => {
// return db.notes?.topicReferences
// .get(topic.id)
// .map((id: string) => db.notes?.note(id)?.data);
// })
// .flat()
// .filter(
// (topicNote) =>
// notes.findIndex((note) => note?.id !== topicNote?.id) === -1
// ) as Note[];
// return [...notes, ...topicNotes];
// }
// });
};
const PLACEHOLDER_DATA = {
heading: params.current.item?.title,
title: params.current.item?.title,
paragraph: "You have not added any notes yet.",
button: "Add your first note",
action: openEditor,
@@ -154,32 +151,33 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
<>
<DelayLayout>
<List
listData={notes}
type="notes"
refreshCallback={() => {
data={notes}
dataType="note"
onRefresh={() => {
onRequestUpdate();
}}
screen="Notebook"
headerProps={{
heading: params.current.title
}}
loading={false}
ListHeader={
renderedInRoute="Notebook"
headerTitle={params.current.title}
loading={loading}
CustomLisHeader={
<NotebookHeader
onEditNotebook={() => {
AddNotebookSheet.present(params.current.item);
}}
notebook={params.current.item}
totalNotes={
notes?.ids.filter((id) => typeof id === "string")?.length || 0
}
/>
}
placeholderData={PLACEHOLDER_DATA}
placeholder={PLACEHOLDER_DATA}
/>
</DelayLayout>
</>
);
};
NotebookScreen.navigate = (item: Notebook, canGoBack: boolean) => {
NotebookScreen.navigate = (item: Notebook, canGoBack?: boolean) => {
if (!item) return;
Navigation.navigate<"Notebook">(
{

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import React, { useEffect } from "react";
import { Config } from "react-native-config";
import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button";
@@ -45,14 +45,6 @@ const prepareSearch = () => {
});
};
const PLACEHOLDER_DATA = {
heading: "Your notebooks",
paragraph: "You have not added any notebooks yet.",
button: "Add your first notebook",
action: onPressFloatingButton,
loading: "Loading your notebooks"
};
export const Notebooks = ({
navigation,
route
@@ -69,12 +61,6 @@ export const Notebooks = ({
});
SearchService.prepareSearch = prepareSearch;
useNavigationStore.getState().setButtonAction(onPressFloatingButton);
//@ts-ignore need to update typings in core to fix this
if (db.notebooks.all.length === 0 && !Config.isTesting) {
Walkthrough.present("notebooks");
} else {
Walkthrough.update("notebooks");
}
return !prev?.current;
},
@@ -82,20 +68,34 @@ export const Notebooks = ({
delay: SettingsService.get().homepage === route.name ? 1 : -1
});
useEffect(() => {
if (notebooks?.ids) {
if (notebooks?.ids?.length === 0 && !Config.isTesting) {
Walkthrough.present("notebooks");
} else {
Walkthrough.update("notebooks");
}
}
}, [notebooks]);
return (
<DelayLayout delay={1}>
<List
listData={notebooks}
type="notebooks"
screen="Notebooks"
data={notebooks}
dataType="notebook"
renderedInRoute="Notebooks"
loading={!isFocused}
placeholderData={PLACEHOLDER_DATA}
headerProps={{
heading: "Notebooks"
placeholder={{
title: "Your notebooks",
paragraph: "You have not added any notebooks yet.",
button: "Add your first notebook",
action: onPressFloatingButton,
loading: "Loading your notebooks"
}}
headerTitle="Notebooks"
/>
{!notebooks || notebooks.length === 0 || !isFocused ? null : (
{!notebooks || notebooks.ids.length === 0 || !isFocused ? null : (
<FloatingButton
title="Create a new notebook"
onPress={onPressFloatingButton}

View File

@@ -34,7 +34,7 @@ export const ColoredNotes = ({
navigation={navigation}
route={route}
get={ColoredNotes.get}
placeholderData={PLACEHOLDER_DATA}
placeholder={PLACEHOLDER_DATA}
onPressFloatingButton={openEditor}
canGoBack={route.params?.canGoBack}
focusControl={true}
@@ -42,11 +42,14 @@ export const ColoredNotes = ({
);
};
ColoredNotes.get = (params: NotesScreenParams, grouped = true) => {
const notes = db.relations.from(params.item, "note").resolved();
return grouped
? groupArray(notes, db.settings.getGroupOptions("notes"))
: notes;
ColoredNotes.get = async (params: NotesScreenParams, grouped = true) => {
if (!grouped) {
return await db.relations.from(params.item, "note").resolve();
}
return await db.relations
.from(params.item, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
};
ColoredNotes.navigate = (item: Color, canGoBack: boolean) => {

View File

@@ -24,7 +24,7 @@ import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useRelationStore } from "../../stores/use-relation-store";
import { useTagStore } from "../../stores/use-tag-store";
import { eOnLoadNote, eOnTopicSheetUpdate } from "../../utils/events";
import { eOnLoadNote, eOnNotebookUpdated } from "../../utils/events";
import { openLinkInBrowser } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { editorController, editorState } from "../editor/tiptap/utils";
@@ -87,24 +87,12 @@ export async function onNoteCreated(noteId: string, data: FirstSaveData) {
);
editorState().onNoteCreated = null;
useRelationStore.getState().update();
break;
}
case "topic": {
if (!data.notebook) break;
await db.notes?.addToNotebook(
{
topic: data.id,
id: data.notebook
},
noteId
);
editorState().onNoteCreated = null;
eSendEvent(eOnTopicSheetUpdate);
eSendEvent(eOnNotebookUpdated, data.id);
break;
}
case "tag": {
const note = db.notes.note(noteId)?.data;
const tag = db.tags.tag(data.id);
const note = await db.notes.note(noteId);
const tag = await db.tags.tag(data.id);
if (tag && note) {
await db.relations.add(tag, note);
@@ -116,8 +104,8 @@ export async function onNoteCreated(noteId: string, data: FirstSaveData) {
break;
}
case "color": {
const note = db.notes.note(noteId)?.data;
const color = db.colors.color(data.id);
const note = await db.notes.note(noteId);
const color = await db.colors.color(data.id);
if (note && color) {
await db.relations.add(color, note);
}

View File

@@ -17,7 +17,13 @@ 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, GroupedItems, Item, Topic } from "@notesnook/core/dist/types";
import {
Color,
GroupedItems,
Item,
Note,
Topic
} from "@notesnook/core/dist/types";
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
@@ -48,12 +54,14 @@ import {
setOnFirstSave,
toCamelCase
} from "./common";
import { PlaceholderData } from "../../components/list/empty";
import { VirtualizedGrouping } from "@notesnook/core";
export const WARNING_DATA = {
title: "Some notes in this topic are not synced"
};
export const PLACEHOLDER_DATA = {
heading: "Your notes",
title: "Your notes",
paragraph: "You have not added any notes yet.",
button: "Add your first Note",
action: openEditor,
@@ -71,8 +79,11 @@ export const MONOGRAPH_PLACEHOLDER_DATA = {
};
export interface RouteProps<T extends RouteName> extends NavigationProps<T> {
get: (params: NotesScreenParams, grouped?: boolean) => GroupedItems<Item>;
placeholderData: unknown;
get: (
params: NotesScreenParams,
grouped?: boolean
) => Promise<VirtualizedGrouping<Note> | Note[]>;
placeholder: PlaceholderData;
onPressFloatingButton: () => void;
focusControl?: boolean;
canGoBack?: boolean;
@@ -91,7 +102,7 @@ const NotesPage = ({
route,
navigation,
get,
placeholderData,
placeholder,
onPressFloatingButton,
focusControl = true,
rightButtons
@@ -99,17 +110,19 @@ const NotesPage = ({
"NotesPage" | "TaggedNotes" | "Monographs" | "ColoredNotes" | "TopicNotes"
>) => {
const params = useRef<NotesScreenParams>(route?.params);
const [notes, setNotes] = useState(get(route.params, true));
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const loading = useNoteStore((state) => state.loading);
const [loadingNotes, setLoadingNotes] = useState(false);
const [loadingNotes, setLoadingNotes] = useState(true);
const isMonograph = route.name === "Monographs";
const notebook =
route.name === "TopicNotes" &&
params.current.item.type === "topic" &&
params.current.item.notebookId
? db.notebooks?.notebook((params.current.item as Topic).notebookId)?.data
: null;
// const notebook =
// route.name === "TopicNotes" &&
// params.current.item.type === "topic" &&
// params.current.item.notebookId
// ? db.notebooks?.notebook((params.current.item as Topic).notebookId)?.data
// : null;
const isFocused = useNavigationFocus(navigation, {
onFocus: (prev) => {
@@ -176,7 +189,7 @@ const NotesPage = ({
]);
const onRequestUpdate = React.useCallback(
(data?: NotesScreenParams) => {
async (data?: NotesScreenParams) => {
const isNew = data && data?.item?.id !== params.current?.item?.id;
if (data) params.current = data;
params.current.title =
@@ -185,15 +198,19 @@ const NotesPage = ({
const { item } = params.current;
try {
if (isNew) setLoadingNotes(true);
const notes = get(params.current, true);
const notes = (await get(
params.current,
true
)) as VirtualizedGrouping<Note>;
if (
((item.type === "tag" || item.type === "color") &&
(!notes || notes.length === 0)) ||
(!notes || notes.ids.length === 0)) ||
(item.type === "topic" && !notes)
) {
return Navigation.goBack();
}
if (notes.length === 0) setLoadingNotes(false);
if (notes.ids.length === 0) setLoadingNotes(false);
setNotes(notes);
syncWithNavigation();
} catch (e) {
@@ -204,10 +221,18 @@ const NotesPage = ({
);
useEffect(() => {
if (loadingNotes) {
setTimeout(() => setLoadingNotes(false), 50);
if (loadingNotes && !loading) {
get(params.current, true)
.then((items) => {
setNotes(items as VirtualizedGrouping<Note>);
setLoadingNotes(false);
})
.catch((e) => {
console.log("Error loading notes", params.current?.title, e, e.stack);
setLoadingNotes(false);
});
}
}, [loadingNotes, notes]);
}, [loadingNotes, loading, get]);
useEffect(() => {
eSubscribeEvent(route.name, onRequestUpdate);
@@ -221,20 +246,18 @@ const NotesPage = ({
<DelayLayout
color={
route.name === "ColoredNotes"
? (params.current?.item as Color).title.toLowerCase()
? (params.current?.item as Color)?.colorCode
: undefined
}
wait={loading || loadingNotes}
>
{route.name === "TopicNotes" ? (
{/* {route.name === "TopicNotes" ? (
<View
style={{
width: "100%",
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center"
// borderBottomWidth: 1,
// borderBottomColor: colors.secondary.background
}}
>
<Paragraph
@@ -266,27 +289,33 @@ const NotesPage = ({
</>
) : null}
</View>
) : null}
) : null} */}
<List
listData={notes}
type="notes"
refreshCallback={onRequestUpdate}
data={notes}
dataType="note"
onRefresh={onRequestUpdate}
loading={loading || !isFocused}
screen="Notes"
headerProps={{
heading: params.current.title,
color:
renderedInRoute="Notes"
headerTitle={params.current.title}
customAccentColor={
route.name === "ColoredNotes"
? (params.current?.item as Color).title.toLowerCase()
: null
}}
placeholderData={placeholderData}
? (params.current?.item as Color)?.colorCode
: undefined
}
placeholder={placeholder}
/>
{!isMonograph &&
route.name !== "TopicNotes" &&
(notes?.length > 0 || isFocused) ? (
<FloatingButton title="Create a note" onPress={onPressFloatingButton} />
((notes?.ids && (notes?.ids?.length || 0) > 0) || isFocused) ? (
<FloatingButton
color={
route.name === "ColoredNotes"
? (params.current?.item as Color)?.colorCode
: undefined
}
title="Create a note"
onPress={onPressFloatingButton}
/>
) : null}
</DelayLayout>
);

View File

@@ -17,7 +17,6 @@ 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 { groupArray } from "@notesnook/core/dist/utils/grouping";
import React from "react";
import NotesPage, { PLACEHOLDER_DATA } from ".";
import { db } from "../../common/database";
@@ -34,7 +33,7 @@ export const Monographs = ({
navigation={navigation}
route={route}
get={Monographs.get}
placeholderData={PLACEHOLDER_DATA}
placeholder={PLACEHOLDER_DATA}
onPressFloatingButton={openMonographsWebpage}
canGoBack={route.params?.canGoBack}
focusControl={true}
@@ -42,11 +41,12 @@ export const Monographs = ({
);
};
Monographs.get = (params?: NotesScreenParams, grouped = true) => {
const notes = db.monographs?.all || [];
return grouped
? groupArray(notes, db.settings.getGroupOptions("notes"))
: notes;
Monographs.get = async (params?: NotesScreenParams, grouped = true) => {
if (!grouped) {
return await db.monographs.all.items();
}
return await db.monographs.all.grouped(db.settings.getGroupOptions("notes"));
};
Monographs.navigate = (item?: MonographType, canGoBack?: boolean) => {

View File

@@ -17,14 +17,13 @@ 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 { groupArray } from "@notesnook/core/dist/utils/grouping";
import { Tag } from "@notesnook/core/dist/types";
import React from "react";
import NotesPage, { PLACEHOLDER_DATA } from ".";
import { db } from "../../common/database";
import Navigation, { NavigationProps } from "../../services/navigation";
import { NotesScreenParams } from "../../stores/use-navigation-store";
import { openEditor } from "./common";
import { Tag } from "@notesnook/core/dist/types";
export const TaggedNotes = ({
navigation,
route
@@ -34,7 +33,7 @@ export const TaggedNotes = ({
navigation={navigation}
route={route}
get={TaggedNotes.get}
placeholderData={PLACEHOLDER_DATA}
placeholder={PLACEHOLDER_DATA}
onPressFloatingButton={openEditor}
canGoBack={route.params?.canGoBack}
focusControl={true}
@@ -42,11 +41,14 @@ export const TaggedNotes = ({
);
};
TaggedNotes.get = (params: NotesScreenParams, grouped = true) => {
const notes = db.relations.from(params.item, "note").resolved();
return grouped
? groupArray(notes, db.settings.getGroupOptions("notes"))
: notes;
TaggedNotes.get = async (params: NotesScreenParams, grouped = true) => {
if (!grouped) {
return await db.relations.from(params.item, "note").resolve();
}
return await db.relations
.from(params.item, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
};
TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => {

View File

@@ -23,10 +23,9 @@ import React from "react";
import NotesPage, { PLACEHOLDER_DATA } from ".";
import { db } from "../../common/database";
import { MoveNotes } from "../../components/sheets/move-notes/movenote";
import { eSendEvent } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import { NotesScreenParams } from "../../stores/use-navigation-store";
import { eOpenAddTopicDialog } from "../../utils/events";
import { openEditor } from "./common";
const headerRightButtons = (params: NotesScreenParams) => [
@@ -35,10 +34,10 @@ const headerRightButtons = (params: NotesScreenParams) => [
onPress: () => {
const { item } = params;
if (item.type !== "topic") return;
eSendEvent(eOpenAddTopicDialog, {
notebookId: item.notebookId,
toEdit: item
});
// eSendEvent(eOpenAddTopicDialog, {
// notebookId: item.notebookId,
// toEdit: item
// });
}
},
{

View File

@@ -40,7 +40,7 @@ const prepareSearch = () => {
};
const PLACEHOLDER_DATA = {
heading: "Your reminders",
title: "Your reminders",
paragraph: "You have not set any reminders yet.",
button: "Set a new reminder",
action: () => {
@@ -76,14 +76,12 @@ export const Reminders = ({
return (
<DelayLayout>
<List
listData={reminders}
type="reminders"
headerProps={{
heading: "Reminders"
}}
data={reminders}
dataType="reminder"
headerTitle="Reminders"
renderedInRoute="Reminders"
loading={!isFocused}
screen="Reminders"
placeholderData={PLACEHOLDER_DATA}
placeholder={PLACEHOLDER_DATA}
/>
<FloatingButton

View File

@@ -36,13 +36,6 @@ const prepareSearch = () => {
});
};
const PLACEHOLDER_DATA = {
heading: "Your tags",
paragraph: "You have not created any tags for your notes yet.",
button: null,
loading: "Loading your tags."
};
export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => {
const tags = useTagStore((state) => state.tags);
const isFocused = useNavigationFocus(navigation, {
@@ -65,14 +58,16 @@ export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => {
return (
<DelayLayout>
<List
listData={tags}
type="tags"
headerProps={{
heading: "Tags"
}}
data={tags}
dataType="tag"
headerTitle="Tags"
loading={!isFocused}
screen="Tags"
placeholderData={PLACEHOLDER_DATA}
renderedInRoute="Tags"
placeholder={{
title: "Your tags",
paragraph: "You have not created any tags for your notes yet.",
loading: "Loading your tags."
}}
/>
</DelayLayout>
);

View File

@@ -61,7 +61,7 @@ const onPressFloatingButton = () => {
});
};
const PLACEHOLDER_DATA = (trashCleanupInterval = 7) => ({
heading: "Trash",
title: "Trash",
paragraph:
trashCleanupInterval === -1
? "Set automatic trash cleanup interval from Settings > Behaviour > Clean trash interval."
@@ -70,7 +70,6 @@ const PLACEHOLDER_DATA = (trashCleanupInterval = 7) => ({
? "daily."
: `after ${trashCleanupInterval} days.`
}`,
button: null,
loading: "Loading trash items"
});
@@ -85,7 +84,10 @@ export const Trash = ({ navigation, route }: NavigationProps<"Trash">) => {
useNavigationStore.getState().update({
name: route.name
});
if (useTrashStore.getState().trash.length === 0) {
if (
!useTrashStore.getState().trash ||
useTrashStore.getState().trash?.ids?.length === 0
) {
useTrashStore.getState().setTrash();
}
SearchService.prepareSearch = prepareSearch;
@@ -97,24 +99,15 @@ export const Trash = ({ navigation, route }: NavigationProps<"Trash">) => {
return (
<DelayLayout>
<List
listData={trash}
type="trash"
screen="Trash"
data={trash}
dataType="trash"
renderedInRoute="Trash"
loading={!isFocused}
placeholderData={PLACEHOLDER_DATA(
db.settings.getTrashCleanupInterval()
)}
headerProps={{
heading: "Trash",
color: null
}}
// TODO: remove these once we have full typings everywhere
ListHeader={undefined}
refreshCallback={undefined}
warning={undefined}
placeholder={PLACEHOLDER_DATA(db.settings.getTrashCleanupInterval())}
headerTitle="Trash"
/>
{trash && trash.length !== 0 ? (
{trash && trash?.ids?.length !== 0 ? (
<FloatingButton
title="Clear all trash"
onPress={onPressFloatingButton}

View File

@@ -225,7 +225,7 @@ async function run(progress, context) {
progress && eSendEvent(eCloseSheet);
}
ToastEvent.show({
ToastManager.show({
heading: "Backup successful",
message: "Your backup is stored in Notesnook folder on your phone.",
type: "success",
@@ -236,7 +236,7 @@ async function run(progress, context) {
} catch (e) {
await sleep(300);
progress && eSendEvent(eCloseSheet);
ToastEvent.error(e, "Backup failed!");
ToastManager.error(e, "Backup failed!");
return null;
}
}

View File

@@ -50,23 +50,6 @@ import { encodeNonAsciiHTML } from "entities";
import { convertNoteToText } from "../utils/note-to-text";
import { Reminder } from "@notesnook/core/dist/types";
// export type Reminder = {
// id: string;
// type: string;
// title: string;
// description?: string;
// priority: "silent" | "vibrate" | "urgent";
// date: number;
// mode: "repeat" | "once" | "permanent";
// recurringMode?: "week" | "month" | "day";
// selectedDays?: number[];
// dateCreated: number;
// dateModified: number;
// localOnly?: boolean;
// snoozeUntil?: number;
// disabled?: boolean;
// };
let pinned: DisplayedNotification[] = [];
/**
@@ -124,7 +107,9 @@ const onEvent = async ({ type, detail }: Event) => {
const { notification, pressAction, input } = detail;
if (type === EventType.DELIVERED && Platform.OS === "android") {
if (notification?.id) {
const reminder = db.reminders?.reminder(notification?.id?.split("_")[0]);
const reminder = await db.reminders?.reminder(
notification?.id?.split("_")[0]
);
if (reminder && reminder.recurringMode === "month") {
await initDatabase();
@@ -172,7 +157,7 @@ const onEvent = async ({ type, detail }: Event) => {
case "REMINDER_SNOOZE": {
await initDatabase();
if (!notification?.id) break;
const reminder = db.reminders?.reminder(
const reminder = await db.reminders?.reminder(
notification?.id?.split("_")[0]
);
if (!reminder) break;
@@ -185,7 +170,7 @@ const onEvent = async ({ type, detail }: Event) => {
snoozeUntil: Date.now() + reminderTime * 60000
});
await Notifications.scheduleNotification(
db.reminders?.reminder(reminder?.id)
await db.reminders?.reminder(reminder?.id)
);
useRelationStore.getState().update();
useReminderStore.getState().setReminders();
@@ -194,7 +179,7 @@ const onEvent = async ({ type, detail }: Event) => {
case "REMINDER_DISABLE": {
await initDatabase();
if (!notification?.id) break;
const reminder = db.reminders?.reminder(
const reminder = await db.reminders?.reminder(
notification?.id?.split("_")[0]
);
await db.reminders?.add({
@@ -203,7 +188,7 @@ const onEvent = async ({ type, detail }: Event) => {
});
if (!reminder?.id) break;
await Notifications.scheduleNotification(
db.reminders?.reminder(reminder?.id)
await db.reminders?.reminder(reminder?.id)
);
useRelationStore.getState().update();
useReminderStore.getState().setReminders();
@@ -253,20 +238,7 @@ const onEvent = async ({ type, detail }: Event) => {
const defaultNotebook = db.settings?.getDefaultNotebook();
if (defaultNotebook) {
if (!defaultNotebook.topic) {
await db.relations?.add(
{ type: "notebook", id: defaultNotebook.id },
{ type: "note", id: id }
);
} else {
await db.notes?.addToNotebook(
{
topic: defaultNotebook.topic,
id: defaultNotebook?.id
},
id
);
}
await db.notes?.addToNotebook(defaultNotebook, id);
}
const status = await NetInfo.fetch();
@@ -441,9 +413,9 @@ async function scheduleNotification(
}
}
function loadNote(id: string, jump: boolean) {
async function loadNote(id: string, jump: boolean) {
if (!id || id === "notesnook_note_input") return;
const note = db.notes?.note(id)?.data;
const note = await db.notes?.note(id);
if (!note) return;
if (!DDS.isTab && jump) {
tabBarRef.current?.goToPage(1);
@@ -871,7 +843,7 @@ async function pinQuickNote(launch: boolean) {
* reschedules them if anything has changed.
*/
async function setupReminders(checkNeedsScheduling = false) {
const reminders = (db.reminders?.all as Reminder[]) || [];
const reminders = ((await db.reminders?.all.items()) as Reminder[]) || [];
const triggers = await notifee.getTriggerNotifications();
for (const reminder of reminders) {

View File

@@ -116,7 +116,7 @@ export class TipManager {
export const useTip = (
context: Context,
fallback: Context,
options: {
options?: {
rotate: boolean;
delay: number;
}

View File

@@ -17,38 +17,26 @@ 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 { GroupedItems, Note } from "@notesnook/core/dist/types";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import create, { State } from "zustand";
import { db } from "../common/database";
import { Note, VirtualizedGrouping } from "@notesnook/core";
export interface FavoriteStore extends State {
favorites: GroupedItems<Note>;
favorites: VirtualizedGrouping<Note> | undefined;
setFavorites: (items?: Note[]) => void;
clearFavorites: () => void;
}
export const useFavoriteStore = create<FavoriteStore>((set, get) => ({
favorites: [],
setFavorites: (items) => {
if (!items) {
export const useFavoriteStore = create<FavoriteStore>((set) => ({
favorites: undefined,
setFavorites: () => {
db.notes.favorites
.grouped(db.settings.getGroupOptions("favorites"))
.then((notes) => {
set({
favorites: groupArray(
db.notes.favorites || [],
db.settings.getGroupOptions("favorites")
)
favorites: notes
});
});
return;
}
const prev = get().favorites;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const index = prev.findIndex((v) => v.id === item.id);
if (index !== -1) {
prev[index] = item;
}
}
set({ favorites: prev });
},
clearFavorites: () => set({ favorites: [] })
clearFavorites: () => set({ favorites: undefined })
}));

View File

@@ -17,12 +17,12 @@ 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 } from "@notesnook/core/dist/types";
import { Color, Notebook, Tag } from "@notesnook/core/dist/types";
import create, { State } from "zustand";
import { db } from "../common/database";
export interface MenuStore extends State {
menuPins: [];
menuPins: (Notebook | Tag)[];
colorNotes: Color[];
setMenuPins: () => void;
setColorNotes: () => void;
@@ -33,18 +33,16 @@ export const useMenuStore = create<MenuStore>((set) => ({
menuPins: [],
colorNotes: [],
setMenuPins: () => {
try {
set({ menuPins: [...(db.shortcuts?.resolved as [])] });
} catch (e) {
setTimeout(() => {
try {
set({ menuPins: [...(db.shortcuts?.resolved as [])] });
} catch (e) {
console.error(e);
}
}, 1000);
}
db.shortcuts.resolved().then((shortcuts) => {
set({ menuPins: [...(shortcuts as [])] });
});
},
setColorNotes: () => {
db.colors?.all.items().then((colors) => {
set({
colorNotes: colors
});
});
},
setColorNotes: () => set({ colorNotes: db.colors?.all || [] }),
clearAll: () => set({ menuPins: [], colorNotes: [] })
}));

View File

@@ -41,6 +41,7 @@ export type Message = {
onPress: () => void;
data: object;
icon: string;
type?: string;
};
export type Action = {
@@ -93,7 +94,8 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
actionText: null,
onPress: () => null,
data: {},
icon: "account-outline"
icon: "account-outline",
type: ""
},
setMessage: (message) => {
set({ message: { ...message } });

View File

@@ -17,38 +17,26 @@ 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 { groupArray } from "@notesnook/core/dist/utils/grouping";
import create, { State } from "zustand";
import { db } from "../common/database";
import { GroupedItems, Notebook } from "@notesnook/core/dist/types";
import { VirtualizedGrouping, Notebook } from "@notesnook/core";
export interface NotebookStore extends State {
notebooks: GroupedItems<Notebook>;
notebooks: VirtualizedGrouping<Notebook> | undefined;
setNotebooks: (items?: Notebook[]) => void;
clearNotebooks: () => void;
}
export const useNotebookStore = create<NotebookStore>((set, get) => ({
notebooks: [],
setNotebooks: (items) => {
if (!items) {
export const useNotebookStore = create<NotebookStore>((set) => ({
notebooks: undefined,
setNotebooks: () => {
db.notebooks.roots
.grouped(db.settings.getGroupOptions("notebooks"))
.then((notebooks) => {
set({
notebooks: groupArray(
db.notebooks.all || [],
db.settings.getGroupOptions("notebooks")
)
notebooks: notebooks
});
});
return;
}
const prev = get().notebooks;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const index = prev.findIndex((v) => v.id === item.id);
if (index !== -1) {
prev[index] = item;
}
}
set({ notebooks: prev });
},
clearNotebooks: () => set({ notebooks: [] })
clearNotebooks: () => set({ notebooks: undefined })
}));

View File

@@ -17,43 +17,28 @@ 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 { groupArray } from "@notesnook/core/dist/utils/grouping";
import { Note, VirtualizedGrouping } from "@notesnook/core";
import create, { State } from "zustand";
import { db } from "../common/database";
import { GroupedItems, Note } from "@notesnook/core/dist/types";
export interface NoteStore extends State {
notes: GroupedItems<Note>;
notes: VirtualizedGrouping<Note> | undefined;
loading: boolean;
setLoading: (loading: boolean) => void;
setNotes: (items?: Note[]) => void;
setNotes: () => void;
clearNotes: () => void;
}
export const useNoteStore = create<NoteStore>((set, get) => ({
notes: [],
export const useNoteStore = create<NoteStore>((set) => ({
notes: undefined,
loading: true,
setLoading: (loading) => set({ loading: loading }),
setNotes: (items) => {
if (!items) {
setNotes: () => {
db.notes.all.grouped(db.settings.getGroupOptions("home")).then((notes) => {
set({
notes: groupArray(
db.notes.all || [],
db.settings.getGroupOptions("home")
)
notes: notes
});
});
return;
}
const prev = get().notes;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const index = prev.findIndex((v) => v.id === item.id);
if (index !== -1) {
prev[index] = item;
}
}
set({ notes: prev });
},
clearNotes: () => set({ notes: [] })
clearNotes: () => set({ notes: undefined })
}));

View File

@@ -17,26 +17,26 @@ 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 { groupReminders } from "@notesnook/core/dist/utils/grouping";
import create, { State } from "zustand";
import { db } from "../common/database";
import { GroupedItems, Reminder } from "@notesnook/core/dist/types";
import { Reminder, VirtualizedGrouping } from "@notesnook/core";
export interface ReminderStore extends State {
reminders: GroupedItems<Reminder>;
reminders: VirtualizedGrouping<Reminder> | undefined;
setReminders: (items?: Reminder[]) => void;
cleareReminders: () => void;
}
export const useReminderStore = create<ReminderStore>((set) => ({
reminders: [],
reminders: undefined,
setReminders: () => {
db.reminders.all
.grouped(db.settings.getGroupOptions("reminders"))
.then((reminders) => {
set({
reminders: groupReminders(
(db.reminders?.all as Reminder[]) || [],
db.settings?.getGroupOptions("reminders")
)
reminders: reminders
});
});
},
cleareReminders: () => set({ reminders: [] })
cleareReminders: () => set({ reminders: undefined })
}));

View File

@@ -17,36 +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 "@notesnook/core/dist/types";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import create, { State } from "zustand";
import { db } from "../common/database";
import { GroupedItems, Tag } from "@notesnook/core/dist/types";
import { Tag, VirtualizedGrouping } from "@notesnook/core";
export interface TagStore extends State {
tags: GroupedItems<Tag>;
tags: VirtualizedGrouping<Tag> | undefined;
setTags: (items?: Tag[]) => void;
clearTags: () => void;
}
export const useTagStore = create<TagStore>((set, get) => ({
tags: [],
setTags: (items) => {
if (!items) {
export const useTagStore = create<TagStore>((set) => ({
tags: undefined,
setTags: () => {
db.tags.all.grouped(db.settings.getGroupOptions("tags")).then((tags) => {
set({
tags: groupArray(db.tags.all || [], db.settings.getGroupOptions("tags"))
tags: tags
});
});
return;
}
const prev = get().tags;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const index = prev.findIndex((v) => v.id === item.id);
if (index !== -1) {
prev[index] = item;
}
}
set({ tags: prev });
},
clearTags: () => set({ tags: [] })
clearTags: () => set({ tags: undefined })
}));

View File

@@ -17,38 +17,25 @@ 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 { groupArray } from "@notesnook/core/dist/utils/grouping";
import create, { State } from "zustand";
import { db } from "../common/database";
import { GroupedItems, TrashItem } from "@notesnook/core/dist/types";
import { VirtualizedGrouping } from "@notesnook/core";
export interface TrashStore extends State {
trash: GroupedItems<TrashItem>;
trash: VirtualizedGrouping<TrashItem> | undefined;
setTrash: (items?: GroupedItems<TrashItem>) => void;
clearTrash: () => void;
}
export const useTrashStore = create<TrashStore>((set, get) => ({
trash: [],
setTrash: (items) => {
if (!items) {
trash: undefined,
setTrash: () => {
db.trash.grouped(db.settings.getGroupOptions("trash")).then((trash) => {
set({
trash: groupArray(
(db.trash.all as TrashItem[]) || [],
db.settings.getGroupOptions("trash")
)
trash: trash
});
});
return;
}
const prev = get().trash;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const index = prev.findIndex((v) => v.id === item.id);
if (index !== -1) {
prev[index] = item;
}
}
set({ trash: prev });
},
clearTrash: () => set({ trash: [] })
clearTrash: () => set({ trash: undefined })
}));

View File

@@ -35,10 +35,6 @@ export const eOpenAddNotebookDialog = "507";
export const eCloseAddNotebookDialog = "508";
export const eOpenAddTopicDialog = "509";
export const eCloseAddTopicDialog = "510";
export const eOpenLoginDialog = "511";
export const eCloseLoginDialog = "512";
@@ -159,8 +155,9 @@ export const eCloseAnnouncementDialog = "604";
export const eOpenLoading = "605";
export const eCloseLoading = "606";
export const eOnTopicSheetUpdate = "607";
export const eOnNotebookUpdated = "607";
export const eUserLoggedIn = "608";
export const eLoginSessionExpired = "609";
export const eDBItemUpdate = "610";

View File

@@ -26,7 +26,8 @@ import SearchService from "../services/search";
import { useMenuStore } from "../stores/use-menu-store";
import { useRelationStore } from "../stores/use-relation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { eClearEditor, eOnTopicSheetUpdate } from "./events";
import { eClearEditor, eOnNotebookUpdated } from "./events";
import { getParentNotebookId } from "./notebooks";
function confirmDeleteAllNotes(items, type, context) {
return new Promise((resolve) => {
@@ -57,6 +58,23 @@ function confirmDeleteAllNotes(items, type, context) {
});
}
async function deleteNotebook(id, deleteNotes) {
const notebook = await db.notebooks.notebook(id);
const parentId = getParentNotebookId(id);
if (deleteNotes) {
const noteRelations = await db.relations.from(notebook, "note").get();
await db.notes.delete(...noteRelations.map((relation) => relation.toId));
}
const subnotebooks = await db.relations.from(notebook, "notebook").get();
for (const subnotebook of subnotebooks) {
await deleteNotebook(subnotebook.toId, deleteNotes);
}
await db.notebooks.remove(id);
if (parentId) {
eSendEvent(eOnNotebookUpdated, parentId);
}
}
export const deleteItems = async (item, context) => {
if (item && db.monographs.isPublished(item.id)) {
ToastManager.show({
@@ -68,14 +86,13 @@ export const deleteItems = async (item, context) => {
return;
}
const selectedItemsList = item
const itemsToDelete = item
? [item]
: useSelectionStore.getState().selectedItemsList;
let notes = selectedItemsList.filter((i) => i.type === "note");
let notebooks = selectedItemsList.filter((i) => i.type === "notebook");
let topics = selectedItemsList.filter((i) => i.type === "topic");
let reminders = selectedItemsList.filter((i) => i.type === "reminder");
let notes = itemsToDelete.filter((i) => i.type === "note");
let notebooks = itemsToDelete.filter((i) => i.type === "notebook");
let reminders = itemsToDelete.filter((i) => i.type === "reminder");
if (reminders.length > 0) {
for (let reminder of reminders) {
@@ -100,59 +117,20 @@ export const deleteItems = async (item, context) => {
eSendEvent(eClearEditor);
}
if (topics?.length > 0) {
const result = await confirmDeleteAllNotes(topics, "topic", context);
if (!result.delete) return;
for (const topic of topics) {
if (result.deleteNotes) {
const notes = db.notebooks
.notebook(topic.notebookId)
.topics.topic(topic.id).all;
await db.notes.delete(...notes.map((note) => note.id));
}
await db.notebooks.notebook(topic.notebookId).topics.delete(topic.id);
}
useMenuStore.getState().setMenuPins();
ToastEvent.show({
heading: `${topics.length > 1 ? "Topics" : "Topic"} deleted`,
type: "success"
});
}
if (notebooks?.length > 0) {
const result = await confirmDeleteAllNotes(notebooks, "notebook", context);
if (!result.delete) return;
let ids = notebooks.map((i) => i.id);
if (result.deleteNotes) {
for (let id of ids) {
const notebook = db.notebooks.notebook(id);
const topics = notebook.topics.all;
for (let topic of topics) {
const notes = db.notebooks
.notebook(topic.notebookId)
.topics.topic(topic.id).all;
await db.notes.delete(...notes.map((note) => note.id));
for (const notebook of notebooks) {
await deleteNotebook(notebook.id, result.deleteNotes);
}
const notes = db.relations.from(notebook.data, "note");
await db.notes.delete(...notes.map((note) => note.id));
}
}
await db.notebooks.delete(...ids);
useMenuStore.getState().setMenuPins();
}
Navigation.queueRoutesForUpdate();
let message = `${selectedItemsList.length} ${
selectedItemsList.length === 1 ? "item" : "items"
let message = `${itemsToDelete.length} ${
itemsToDelete.length === 1 ? "item" : "items"
} moved to trash.`;
let deletedItems = [...selectedItemsList];
if (
topics.length === 0 &&
reminders.length === 0 &&
(notes.length > 0 || notebooks.length > 0)
) {
let deletedItems = [...itemsToDelete];
if (reminders.length === 0 && (notes.length > 0 || notebooks.length > 0)) {
ToastManager.show({
heading: message,
type: "success",
@@ -173,6 +151,7 @@ export const deleteItems = async (item, context) => {
actionText: "Undo"
});
}
Navigation.queueRoutesForUpdate();
if (!item) {
useSelectionStore.getState().clearSelection();
@@ -180,7 +159,6 @@ export const deleteItems = async (item, context) => {
useMenuStore.getState().setMenuPins();
useMenuStore.getState().setColorNotes();
SearchService.updateAndSearch();
eSendEvent(eOnTopicSheetUpdate);
};
export const openLinkInBrowser = async (link) => {

View File

@@ -0,0 +1,32 @@
import { db } from "../common/database";
export async function findRootNotebookId(id: string) {
const relation = await db.relations
.to(
{
id,
type: "notebook"
},
"notebook"
)
.get();
if (!relation || !relation.length) {
return id;
} else {
return findRootNotebookId(relation[0].fromId);
}
}
export async function getParentNotebookId(id: string) {
const relation = await db.relations
.to(
{
id,
type: "notebook"
},
"notebook"
)
.get();
return relation?.[0]?.fromId;
}

View File

@@ -48,3 +48,4 @@ hermesEnabled=true
# fdroid
fdroidBuild=false
quickSqliteFlags=-DSQLITE_ENABLE_FTS5

View File

@@ -6,7 +6,15 @@ const configs = {
plugins: [
'@babel/plugin-transform-named-capturing-groups-regex',
'react-native-reanimated/plugin',
"@babel/plugin-transform-export-namespace-from"
"@babel/plugin-transform-export-namespace-from",
],
overrides: [
{
test: '../node_modules/kysely',
plugins: [
["@babel/plugin-transform-private-methods", { "loose": true }]
]
}
]
},
test: {
@@ -14,8 +22,16 @@ const configs = {
plugins: [
'@babel/plugin-transform-named-capturing-groups-regex',
'react-native-reanimated/plugin',
],
overrides: [
{
test: '../node_modules/kysely',
plugins: [
["@babel/plugin-transform-private-methods", { "loose": true }]
]
}
]
},
production: {
presets: ['module:metro-react-native-babel-preset'],
@@ -23,7 +39,15 @@ const configs = {
'transform-remove-console',
'@babel/plugin-transform-named-capturing-groups-regex',
'react-native-reanimated/plugin',
"@babel/plugin-transform-export-namespace-from"
"@babel/plugin-transform-export-namespace-from",
],
overrides: [
{
test: '../node_modules/kysely',
plugins: [
["@babel/plugin-transform-private-methods", { "loose": true }]
]
}
]
}
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import "./polyfills/console-time.js"
global.Buffer = require('buffer').Buffer;
import '../app/common/logger/index';
import { DOMParser } from './worker.js';
global.DOMParser = DOMParser;

View File

@@ -123,6 +123,13 @@ post_install do |installer|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
end
end
installer.pods_project.targets.each do |target|
if target.name == "react-native-quick-sqlite" then
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'SQLITE_ENABLE_FTS5=1'
end
end
end
installer.pods_project.targets.each do |target|
if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
target.build_configurations.each do |config|

View File

@@ -29,6 +29,7 @@ mergedConfig.resolver = {
"react-dom": path.join(__dirname, "../node_modules/react-dom"),
"@notesnook": path.join(__dirname, "../../../packages"),
"@notifee/react-native": path.join(__dirname, "../node_modules/@ammarahmed/notifee-react-native"),
},
resolveRequest: (context, moduleName, platform) => {
if (moduleName ==='react') {
@@ -38,6 +39,14 @@ mergedConfig.resolver = {
type: 'sourceFile',
};
}
if (moduleName ==='kysely') {
// Resolve react package from mobile app's node_modules folder always.
return {
filePath: path.resolve(path.join(__dirname, '../node_modules', "kysely","dist", "cjs", "index.js")),
type: 'sourceFile',
};
}
return context.resolveRequest(context, moduleName, platform);
}
};

View File

@@ -62,7 +62,8 @@
"react-native-tooltips": "^1.0.3",
"react-native-vector-icons": "9.2.0",
"react-native-webview": "^11.14.1",
"react-native-zip-archive": "6.0.9"
"react-native-zip-archive": "6.0.9",
"react-native-quick-sqlite": "^8.0.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",

View File

@@ -0,0 +1,50 @@
const PerformanceNow =
(global.performance && global.performance.now) ||
global.performanceNow ||
global.nativePerformanceNow || (() => { try {
var now = require('fbjs/lib/performanceNow')
} finally { return now }})();
const DEFAULT_LABEL = 'default';
const DEFAULT_PREC = 3;
let counts = {};
let startTimes = {};
const fixed = n => Math.trunc(n) === n ? n + '' : n.toFixed(DEFAULT_PREC);
console.time = console.time || ((label = DEFAULT_LABEL) => { startTimes[label] = PerformanceNow() });
console.timeLog = console.timeLog || ((label = DEFAULT_LABEL, desc) => timeRecord(label, desc));
console.timeEnd = console.timeEnd || ((label = DEFAULT_LABEL) => timeRecord(label, undefined, true));
console.count = console.count || ((label = DEFAULT_LABEL) => {
if (!counts[label]) {
counts[label] = 0;
}
counts[label]++;
console.log(`${label}: ${counts[label]}`);
});
console.countReset = console.countReset || ((label = DEFAULT_LABEL) => {
if (counts[label]) {
counts[label] = 0;
} else {
console.warn(`Count for '${label}' does not exist`);
}
});
function timeRecord(label, desc, final) {
const endTime = PerformanceNow();
const startTime = startTimes[label];
if (startTime) {
const delta = endTime - startTime;
if (desc) {
console.log(`${label}: ${fixed(delta)}ms ${desc}`);
} else {
console.log(`${label}: ${fixed(delta)}ms`);
}
if (final) delete startTimes[label];
} else {
console.warn(`Timer '${label}' does not exist`);
}
}

Some files were not shown because too many files have changed in this diff Show More