mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
mobile: migrate to sqlite
This commit is contained in:
committed by
Abdullah Atta
parent
2168609577
commit
5bfad0149b
@@ -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 () => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
123
apps/mobile/app/common/database/sqlite.kysely.ts
Normal file
123
apps/mobile/app/common/database/sqlite.kysely.ts
Normal 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 || [] : []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,17 +318,16 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
<Button
|
||||
key={item.name}
|
||||
buttonType={{
|
||||
text: item.on
|
||||
? colors.primary.accent
|
||||
: item.name === "Delete" || item.name === "PermDelete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
text:
|
||||
item.name === "Delete" || item.name === "PermDelete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
onPress={item.onPress}
|
||||
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) => (
|
||||
@@ -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",
|
||||
@@ -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) => (
|
||||
@@ -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} />
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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%",
|
||||
@@ -42,7 +42,6 @@ const ImagePreview = () => {
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("ImagePreview", open);
|
||||
|
||||
return () => {
|
||||
eUnSubscribeEvent("ImagePreview", open);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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,44 +102,45 @@ const NoteItem = ({
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{notebooks?.map((item) => (
|
||||
<Button
|
||||
title={
|
||||
item.title.length > 25
|
||||
? item.title.slice(0, 25) + "..."
|
||||
: item.title
|
||||
}
|
||||
tooltipText={item.title}
|
||||
key={item.id}
|
||||
height={25}
|
||||
icon={item.type === "topic" ? "bookmark" : "book-outline"}
|
||||
type="grayBg"
|
||||
fontSize={SIZE.xs}
|
||||
iconSize={SIZE.sm}
|
||||
textStyle={{
|
||||
marginRight: 0
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
marginRight: 5,
|
||||
borderWidth: 0.5,
|
||||
borderColor: primaryColors.border,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
if (item.type === "topic") {
|
||||
TopicNotes.navigate(item, true);
|
||||
} else {
|
||||
NotebookScreen.navigate(item);
|
||||
{notebooks?.items
|
||||
?.filter(
|
||||
(item) =>
|
||||
item.id !== useNavigationStore.getState().currentScreen?.id
|
||||
)
|
||||
.map((item) => (
|
||||
<Button
|
||||
title={
|
||||
item.title.length > 25
|
||||
? item.title.slice(0, 25) + "..."
|
||||
: item.title
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
tooltipText={item.title}
|
||||
key={item.id}
|
||||
height={25}
|
||||
icon={item.type === "notebook" ? "bookmark" : "book-outline"}
|
||||
type="grayBg"
|
||||
fontSize={SIZE.xs}
|
||||
iconSize={SIZE.sm}
|
||||
textStyle={{
|
||||
marginRight: 0
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
marginRight: 5,
|
||||
borderWidth: 0.5,
|
||||
borderColor: primaryColors.border,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
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,
|
||||
@@ -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;
|
||||
}
|
||||
} else {
|
||||
if (!db.trash.synced(item.id)) {
|
||||
present();
|
||||
return;
|
||||
}
|
||||
note = (await db.notes.note(item.id)) as Note;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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%"
|
||||
@@ -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>
|
||||
</>
|
||||
@@ -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;
|
||||
}
|
||||
269
apps/mobile/app/components/list/list-item.wrapper.tsx
Normal file
269
apps/mobile/app/components/list/list-item.wrapper.tsx
Normal 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
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -53,23 +53,24 @@ export const DateMeta = ({ item }) => {
|
||||
return keys.filter((key) => key.startsWith("date") && key !== "date");
|
||||
}
|
||||
|
||||
const renderItem = (key) => (
|
||||
<View
|
||||
key={key}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 3
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getNameFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getFormattedDate(item[key], "date-time")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
const renderItem = (key) =>
|
||||
!item[key] ? null : (
|
||||
<View
|
||||
key={key}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 3
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getNameFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getFormattedDate(item[key], "date-time")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
173
apps/mobile/app/components/sheets/add-notebook/index.tsx
Normal file
173
apps/mobile/app/components/sheets/add-notebook/index.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
for (let noteId of noteIds) {
|
||||
await db.relations.add(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} 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
|
||||
);
|
||||
for (let noteId of noteIds) {
|
||||
await db.relations.unlink(item, { id: noteId, type: "note" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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
|
||||
});
|
||||
@@ -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,9 +206,16 @@ 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,34 +307,48 @@ const TagItem = ({ tag, notes }: { tag: Tag; notes: Note[] }) => {
|
||||
onPress={onPress}
|
||||
type="gray"
|
||||
>
|
||||
<IconButton
|
||||
size={22}
|
||||
customStyle={{
|
||||
marginRight: 5,
|
||||
width: 23,
|
||||
height: 23
|
||||
}}
|
||||
color={
|
||||
someNotesTagged || allNotesTagged
|
||||
? colors.selected.icon
|
||||
: colors.primary.icon
|
||||
}
|
||||
testID={
|
||||
allNotesTagged
|
||||
? "check-circle-outline"
|
||||
: someNotesTagged
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
name={
|
||||
allNotesTagged
|
||||
? "check-circle-outline"
|
||||
: someNotesTagged
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
<Paragraph size={SIZE.sm}>{"#" + tag.title}</Paragraph>
|
||||
{!tag ? null : (
|
||||
<IconButton
|
||||
size={22}
|
||||
customStyle={{
|
||||
marginRight: 5,
|
||||
width: 23,
|
||||
height: 23
|
||||
}}
|
||||
onPress={onPress}
|
||||
color={
|
||||
selection.some || selection.all
|
||||
? colors.selected.icon
|
||||
: colors.primary.icon
|
||||
}
|
||||
testID={
|
||||
selection.all
|
||||
? "check-circle-outline"
|
||||
: selection.some
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
name={
|
||||
selection.all
|
||||
? "check-circle-outline"
|
||||
: selection.some
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{tag ? (
|
||||
<Paragraph size={SIZE.sm}>{"#" + tag?.title}</Paragraph>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
width: 200,
|
||||
height: 30,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
) : null}
|
||||
</PressableButton>
|
||||
<SelectableNoteItem
|
||||
id={item}
|
||||
items={notes}
|
||||
select={select}
|
||||
selected={selectedNoteIds?.indexOf(item) > -1}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
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."
|
||||
}
|
||||
/>
|
||||
<Seperator />
|
||||
</>
|
||||
)}
|
||||
<DialogHeader
|
||||
title={`Add notes to ${currentNotebook.title}`}
|
||||
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} />
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,45 +157,33 @@ 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()) {
|
||||
ref.current?.snapToIndex(snapPoint);
|
||||
} 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,108 +393,190 @@ 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 (
|
||||
<PressableButton
|
||||
type={isSelected || isFocused ? "selected" : "transparent"}
|
||||
onLongPress={() => {
|
||||
if (selection.enabled) return;
|
||||
selection.setEnabled(true);
|
||||
selection.toggleSelection(item);
|
||||
}}
|
||||
testID={`topic-sheet-item-${index}`}
|
||||
onPress={() => {
|
||||
if (selection.enabled) {
|
||||
selection.toggleSelection(item);
|
||||
return;
|
||||
}
|
||||
TopicNotes.navigate(item, true);
|
||||
}}
|
||||
customStyle={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 0
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: currentLevel > 0 && currentLevel < 6 ? 15 : undefined,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
<PressableButton
|
||||
type={isSelected || isFocused ? "selected" : "transparent"}
|
||||
onLongPress={() => {
|
||||
if (selection.enabled || !item) return;
|
||||
selection.setEnabled(true);
|
||||
selection.toggleSelection(item);
|
||||
}}
|
||||
testID={`topic-sheet-item-${currentLevel}-${index}`}
|
||||
onPress={() => {
|
||||
if (!item) return;
|
||||
if (selection.enabled) {
|
||||
selection.toggleSelection(item);
|
||||
return;
|
||||
}
|
||||
NotebookScreen.navigate(item, true);
|
||||
}}
|
||||
customStyle={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
paddingLeft: 0,
|
||||
paddingRight: 12,
|
||||
borderRadius: 0
|
||||
}}
|
||||
>
|
||||
{selection.enabled ? (
|
||||
<IconButton
|
||||
size={SIZE.lg}
|
||||
color={isSelected ? colors.selected.icon : colors.primary.icon}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
name={
|
||||
isSelected
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Paragraph size={SIZE.sm}>
|
||||
{item.title}{" "}
|
||||
{notesCount ? (
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{notesCount}
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{selection.enabled ? (
|
||||
<IconButton
|
||||
size={SIZE.lg}
|
||||
color={isSelected ? colors.selected.icon : colors.primary.icon}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
name={
|
||||
isSelected
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<IconButton
|
||||
name="dots-horizontal"
|
||||
customStyle={{
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
}}
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
onPress={() => {
|
||||
Properties.present(item);
|
||||
}}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
top={0}
|
||||
color={colors.primary.icon}
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
</PressableButton>
|
||||
|
||||
{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}>
|
||||
{totalNotes(id)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<IconButton
|
||||
name="dots-horizontal"
|
||||
customStyle={{
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
}}
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
onPress={() => {
|
||||
Properties.present(item);
|
||||
}}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
top={0}
|
||||
color={colors.primary.icon}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -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} />
|
||||
)
|
||||
});
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -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)}
|
||||
@@ -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
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await db.shortcuts.add({
|
||||
item: {
|
||||
type: item.type,
|
||||
id: item.id
|
||||
}
|
||||
});
|
||||
}
|
||||
await db.shortcuts.add({
|
||||
itemId: item.id,
|
||||
itemType: item.type
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
close();
|
||||
},
|
||||
on: defaultNotebook?.topic ? false : defaultNotebook?.id === item.id
|
||||
AddNotebookSheet.present(undefined, item);
|
||||
}
|
||||
},
|
||||
{
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
131
apps/mobile/app/hooks/use-db-item.ts
Normal file
131
apps/mobile/app/hooks/use-db-item.ts
Normal 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
|
||||
};
|
||||
};
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
87
apps/mobile/app/hooks/use-notebook.ts
Normal file
87
apps/mobile/app/hooks/use-notebook.ts
Normal 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
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">(
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
route.name === "ColoredNotes"
|
||||
? (params.current?.item as Color).title.toLowerCase()
|
||||
: null
|
||||
}}
|
||||
placeholderData={placeholderData}
|
||||
renderedInRoute="Notes"
|
||||
headerTitle={params.current.title}
|
||||
customAccentColor={
|
||||
route.name === "ColoredNotes"
|
||||
? (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>
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
// });
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -116,7 +116,7 @@ export class TipManager {
|
||||
export const useTip = (
|
||||
context: Context,
|
||||
fallback: Context,
|
||||
options: {
|
||||
options?: {
|
||||
rotate: boolean;
|
||||
delay: number;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
set({
|
||||
favorites: groupArray(
|
||||
db.notes.favorites || [],
|
||||
db.settings.getGroupOptions("favorites")
|
||||
)
|
||||
export const useFavoriteStore = create<FavoriteStore>((set) => ({
|
||||
favorites: undefined,
|
||||
setFavorites: () => {
|
||||
db.notes.favorites
|
||||
.grouped(db.settings.getGroupOptions("favorites"))
|
||||
.then((notes) => {
|
||||
set({
|
||||
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 })
|
||||
}));
|
||||
|
||||
@@ -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: [] })
|
||||
}));
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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) {
|
||||
set({
|
||||
notebooks: groupArray(
|
||||
db.notebooks.all || [],
|
||||
db.settings.getGroupOptions("notebooks")
|
||||
)
|
||||
export const useNotebookStore = create<NotebookStore>((set) => ({
|
||||
notebooks: undefined,
|
||||
setNotebooks: () => {
|
||||
db.notebooks.roots
|
||||
.grouped(db.settings.getGroupOptions("notebooks"))
|
||||
.then((notebooks) => {
|
||||
set({
|
||||
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 })
|
||||
}));
|
||||
|
||||
@@ -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 })
|
||||
}));
|
||||
|
||||
@@ -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: () => {
|
||||
set({
|
||||
reminders: groupReminders(
|
||||
(db.reminders?.all as Reminder[]) || [],
|
||||
db.settings?.getGroupOptions("reminders")
|
||||
)
|
||||
});
|
||||
db.reminders.all
|
||||
.grouped(db.settings.getGroupOptions("reminders"))
|
||||
.then((reminders) => {
|
||||
set({
|
||||
reminders: reminders
|
||||
});
|
||||
});
|
||||
},
|
||||
cleareReminders: () => set({ reminders: [] })
|
||||
cleareReminders: () => set({ reminders: undefined })
|
||||
}));
|
||||
|
||||
@@ -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 })
|
||||
}));
|
||||
|
||||
@@ -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 })
|
||||
}));
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
const notes = db.relations.from(notebook.data, "note");
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
for (const notebook of notebooks) {
|
||||
await deleteNotebook(notebook.id, result.deleteNotes);
|
||||
}
|
||||
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) => {
|
||||
|
||||
32
apps/mobile/app/utils/notebooks.ts
Normal file
32
apps/mobile/app/utils/notebooks.ts
Normal 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;
|
||||
}
|
||||
@@ -47,4 +47,5 @@ hermesEnabled=true
|
||||
# v8.android.tools.dir=/home/ammarahm-ed/Repos/notesnook-mobile/node_modules/v8-android-jit-nointl/dist/tools/android
|
||||
|
||||
# fdroid
|
||||
fdroidBuild=false
|
||||
fdroidBuild=false
|
||||
quickSqliteFlags=-DSQLITE_ENABLE_FTS5
|
||||
@@ -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',
|
||||
["@babel/plugin-transform-private-methods", { "loose": true }]
|
||||
],
|
||||
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 }]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
50
apps/mobile/native/polyfills/console-time.js
Normal file
50
apps/mobile/native/polyfills/console-time.js
Normal 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
Reference in New Issue
Block a user