mobile: migrate to sqlite

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

View File

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

View File

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

View File

@@ -16,14 +16,14 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { initalize } from "@notesnook/core/dist/logger";
import { MMKVLoader } from "react-native-mmkv-storage"; import { MMKVLoader } from "react-native-mmkv-storage";
import { initialize } from "@notesnook/core/dist/logger";
import { KV } from "./storage"; import { KV } from "./storage";
const LoggerStorage = new MMKVLoader() const LoggerStorage = new MMKVLoader()
.withInstanceID("notesnook_logs") .withInstanceID("notesnook_logs")
.initialize(); .initialize();
initalize(new KV(LoggerStorage)); initialize(new KV(LoggerStorage));
export { LoggerStorage }; export { LoggerStorage };

View File

@@ -0,0 +1,123 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import type { DatabaseConnection, Driver, QueryResult } from "kysely";
import { CompiledQuery } from "kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
type Config = { dbName: string; async: boolean; location: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;
private connectionMutex = new ConnectionMutex();
private db: QuickSQLiteConnection;
constructor(private readonly config: Config) {
this.db = open({
name: config.dbName
});
}
async init(): Promise<void> {
this.connection = new RNSqliteConnection(this.db);
}
async acquireConnection(): Promise<DatabaseConnection> {
// SQLite only has one single connection. We use a mutex here to wait
// until the single connection has been released.
await this.connectionMutex.lock();
return this.connection!;
}
async beginTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("begin"));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("commit"));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("rollback"));
}
async releaseConnection(): Promise<void> {
this.connectionMutex.unlock();
}
async destroy(): Promise<void> {
this.db.close();
}
}
class ConnectionMutex {
private promise?: Promise<void>;
private resolve?: () => void;
async lock(): Promise<void> {
while (this.promise) {
await this.promise;
}
this.promise = new Promise((resolve) => {
this.resolve = resolve;
});
}
unlock(): void {
const resolve = this.resolve;
this.promise = undefined;
this.resolve = undefined;
resolve?.();
}
}
class RNSqliteConnection implements DatabaseConnection {
constructor(private readonly db: QuickSQLiteConnection) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error("wasqlite driver doesn't support streaming");
}
async executeQuery<R>(
compiledQuery: CompiledQuery<unknown>
): Promise<QueryResult<R>> {
const { parameters, sql, query } = compiledQuery;
const mode =
query.kind === "SelectQueryNode"
? "query"
: query.kind === "RawNode"
? "raw"
: "exec";
const result = await this.db.executeAsync(sql, parameters as any[]);
console.log("SQLITE result:", result?.rows?._array);
if (mode === "query" || !result.insertId)
return {
rows: result.rows?._array || []
};
return {
insertId: BigInt(result.insertId),
numAffectedRows: BigInt(result.rowsAffected),
rows: mode === "raw" ? result.rows?._array || [] : []
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,15 +17,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { useThemeColors } from "@notesnook/theme";
import React from "react"; import React from "react";
import { useNoteStore } from "../../stores/use-notes-store"; import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeColors } from "@notesnook/theme";
import { AnnouncementDialog } from "../announcements"; import { AnnouncementDialog } from "../announcements";
import AuthModal from "../auth/auth-modal"; import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired"; import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog"; import { Dialog } from "../dialog";
import { AddTopicDialog } from "../dialogs/add-topic"; import { AddTopicDialog } from "../dialogs/add-topic";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { LoadingDialog } from "../dialogs/loading"; import { LoadingDialog } from "../dialogs/loading";
import PDFPreview from "../dialogs/pdf-preview";
import ResultDialog from "../dialogs/result"; import ResultDialog from "../dialogs/result";
import { VaultDialog } from "../dialogs/vault"; import { VaultDialog } from "../dialogs/vault";
import ImagePreview from "../image-preview"; import ImagePreview from "../image-preview";
@@ -36,7 +38,6 @@ import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app"; import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key"; import RecoveryKeySheet from "../sheets/recovery-key";
import RestoreDataSheet from "../sheets/restore-data"; import RestoreDataSheet from "../sheets/restore-data";
import PDFPreview from "../dialogs/pdf-preview";
const DialogProvider = () => { const DialogProvider = () => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
@@ -46,7 +47,6 @@ const DialogProvider = () => {
<> <>
<LoadingDialog /> <LoadingDialog />
<Dialog context="global" /> <Dialog context="global" />
<AddTopicDialog colors={colors} />
<PremiumDialog colors={colors} /> <PremiumDialog colors={colors} />
<AuthModal colors={colors} /> <AuthModal colors={colors} />
<MergeConflicts /> <MergeConflicts />
@@ -62,6 +62,7 @@ const DialogProvider = () => {
<AnnouncementDialog /> <AnnouncementDialog />
<SessionExpired /> <SessionExpired />
<PDFPreview /> <PDFPreview />
<JumpToSectionDialog />
</> </>
); );
}; };

View File

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

View File

@@ -1,194 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { createRef } from "react";
import { View } from "react-native";
import { useMenuStore } from "../../../stores/use-menu-store";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastManager
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { db } from "../../../common/database";
import {
eCloseAddTopicDialog,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogContainer from "../../dialog/dialog-container";
import DialogHeader from "../../dialog/dialog-header";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { Toast } from "../../toast";
import { useRelationStore } from "../../../stores/use-relation-store";
export class AddTopicDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
titleFocused: false,
loading: false
};
this.ref = createRef();
this.title;
this.titleRef = createRef();
this.notebook = null;
this.toEdit = null;
}
addNewTopic = async () => {
try {
if (!this.title || this.title?.trim() === "") {
ToastManager.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return;
}
if (!this.toEdit) {
await db.notebooks.topics(this.notebook.id).add({
title: this.title
});
} else {
let topic = this.toEdit;
topic.title = this.title;
await db.notebooks.topics(topic.notebookId).add({
id: topic.id,
title: topic.title
});
}
this.close();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
useMenuStore.getState().setMenuPins();
});
eSendEvent(eOnTopicSheetUpdate);
useRelationStore.getState().update();
} catch (e) {
console.error(e);
}
};
componentDidMount() {
eSubscribeEvent(eOpenAddTopicDialog, this.open);
eSubscribeEvent(eCloseAddTopicDialog, this.close);
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenAddTopicDialog, this.open);
eUnSubscribeEvent(eCloseAddTopicDialog, this.close);
}
open = async ({ notebookId, toEdit }) => {
let id = notebookId;
if (id) {
this.notebook = await db.notebooks.notebook(id).data;
}
this.toEdit = toEdit;
if (this.toEdit) {
this.title = this.toEdit.title;
}
this.setState({
visible: true
});
};
close = () => {
this.title = null;
this.notebook = null;
this.toEdit = null;
this.setState({
visible: false
});
};
render() {
const { visible } = this.state;
if (!visible) return null;
return (
<BaseDialog
onShow={async () => {
if (this.toEdit) {
this.titleRef.current?.setNativeProps({
text: this.toEdit.title
});
}
this.ref.current?.animateNextTransition();
await sleep(300);
this.titleRef.current?.focus();
}}
bounce={false}
statusBarTranslucent={false}
visible={true}
onRequestClose={this.close}
>
<DialogContainer>
<DialogHeader
icon="book-outline"
title={this.toEdit ? "Edit topic" : "New topic"}
paragraph={
this.toEdit
? "Edit title of the topic"
: "Add a new topic in " + this.notebook.title
}
padding={12}
/>
<Seperator half />
<View
style={{
paddingHorizontal: 12,
zIndex: 10
}}
>
<Input
fwdRef={this.titleRef}
testID="input-title"
onChangeText={(value) => {
this.title = value;
}}
blurOnSubmit={false}
placeholder="Enter title"
onSubmit={() => this.addNewTopic()}
returnKeyLabel="Done"
returnKeyType="done"
/>
</View>
<DialogButtons
positiveTitle={this.toEdit ? "Save" : "Add"}
onPressNegative={() => this.close()}
onPressPositive={() => this.addNewTopic()}
/>
</DialogContainer>
<Toast context="local" />
</BaseDialog>
);
}
}

View File

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

View File

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

View File

@@ -17,57 +17,39 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { useThemeColors } from "@notesnook/theme";
import React from "react"; import React from "react";
import { View } from "react-native"; import { View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { useMessageStore } from "../../../stores/use-message-store"; 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 { Announcement } from "../../announcements/announcement";
import { Card } from "../../list/card"; import { Card } from "../../list/card";
import Paragraph from "../../ui/typography/paragraph";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; export type ListHeaderProps = {
import { SIZE } from "../../../utils/size"; noAnnouncement?: boolean;
import { useSelectionStore } from "../../../stores/use-selection-store"; color?: string;
messageCard?: boolean;
screen?: string;
shouldShow?: boolean;
};
export const Header = React.memo( export const Header = React.memo(
({ ({
type,
messageCard = true, messageCard = true,
color, color,
shouldShow = false, shouldShow = false,
noAnnouncement, noAnnouncement,
warning screen
}) => { }: ListHeaderProps) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const announcements = useMessageStore((state) => state.announcements); const announcements = useMessageStore((state) => state.announcements);
const selectionMode = useSelectionStore((state) => state.selectionMode); const selectionMode = useSelectionStore((state) => state.selectionMode);
return selectionMode ? null : ( return selectionMode ? null : (
<> <>
{warning ? ( {announcements.length !== 0 && !noAnnouncement ? (
<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 ? (
<Announcement color={color || colors.primary.accent} /> <Announcement color={color || colors.primary.accent} />
) : type === "search" ? null : !shouldShow ? ( ) : (screen as any) === "Search" ? null : !shouldShow ? (
<View <View
style={{ style={{
marginBottom: 5, marginBottom: 5,
@@ -78,11 +60,7 @@ export const Header = React.memo(
}} }}
> >
{messageCard ? ( {messageCard ? (
<Card <Card color={color || colors.primary.accent} />
color={
ColorValues[color?.toLowerCase()] || colors.primary.accent
}
/>
) : null} ) : null}
</View> </View>
) : null} ) : null}

View File

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

View File

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

View File

@@ -17,21 +17,30 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { getUpcomingReminder } from "@notesnook/core/dist/collections/reminders"; import {
import { decode, EntityLevel } from "entities"; BaseTrashItem,
Color,
Note,
Reminder,
TrashItem
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { EntityLevel, decode } from "entities";
import React from "react"; import React from "react";
import { View } from "react-native"; import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids"; 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 NotebookScreen from "../../../screens/notebook";
import { TaggedNotes } from "../../../screens/notes/tagged"; 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 useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store"; import { useRelationStore } from "../../../stores/use-relation-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../../utils/colors";
import { SIZE } from "../../../utils/size"; import { SIZE } from "../../../utils/size";
import {
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../list/list-item.wrapper";
import { Properties } from "../../properties"; import { Properties } from "../../properties";
import { Button } from "../../ui/button"; import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button"; import { IconButton } from "../../ui/icon-button";
@@ -39,73 +48,39 @@ import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since"; import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading"; import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useEditorStore } from "../../../stores/use-editor-store";
function navigateToTag(item) { type NoteItemProps = {
const tag = db.tags.tag(item.id); item: Note | BaseTrashItem<Note>;
if (!tag) return; index: number;
TaggedNotes.navigate(tag, true); tags?: TagsWithDateEdited;
} notebooks?: NotebooksWithDateEdited;
color?: Color;
const showActionSheet = (item) => { reminder?: Reminder;
Properties.present(item); 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 = ({ const NoteItem = ({
item, item,
isTrash, isTrash,
dateBy = "dateCreated", date,
color,
notebooks,
reminder,
tags,
attachmentsCount,
noOpen = false noOpen = false
}) => { }: NoteItemProps) => {
const isEditingNote = useEditorStore( const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id (state) => state.currentEditingNote === item.id
); );
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(item); const compactMode = useIsCompactModeEnabled(
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0; (item as TrashItem).itemType || item.type
);
const _update = useRelationStore((state) => state.updater); 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; const primaryColors = isEditingNote ? colors.selected : colors.primary;
return ( return (
@@ -127,44 +102,45 @@ const NoteItem = ({
flexWrap: "wrap" flexWrap: "wrap"
}} }}
> >
{notebooks?.map((item) => ( {notebooks?.items
<Button ?.filter(
title={ (item) =>
item.title.length > 25 item.id !== useNavigationStore.getState().currentScreen?.id
? item.title.slice(0, 25) + "..." )
: item.title .map((item) => (
} <Button
tooltipText={item.title} title={
key={item.id} item.title.length > 25
height={25} ? item.title.slice(0, 25) + "..."
icon={item.type === "topic" ? "bookmark" : "book-outline"} : item.title
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);
} }
}} 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 <ReminderTime
reminder={reminder} reminder={reminder}
color={noteColor} color={color?.colorCode}
onPress={() => { onPress={() => {
Properties.present(reminder); Properties.present(reminder);
}} }}
@@ -178,9 +154,7 @@ const NoteItem = ({
{compactMode ? ( {compactMode ? (
<Paragraph <Paragraph
numberOfLines={1} numberOfLines={1}
color={ color={color?.colorCode || primaryColors.heading}
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
style={{ style={{
flexWrap: "wrap" flexWrap: "wrap"
}} }}
@@ -191,9 +165,7 @@ const NoteItem = ({
) : ( ) : (
<Heading <Heading
numberOfLines={1} numberOfLines={1}
color={ color={color?.colorCode || primaryColors.heading}
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
style={{ style={{
flexWrap: "wrap" flexWrap: "wrap"
}} }}
@@ -246,13 +218,11 @@ const NoteItem = ({
color: colors.secondary.paragraph, color: colors.secondary.paragraph,
marginRight: 6 marginRight: 6
}} }}
time={item[dateBy]} time={date}
updateFrequency={ updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
}
/> />
{attachmentCount > 0 ? ( {attachmentsCount > 0 ? (
<View <View
style={{ style={{
flexDirection: "row", flexDirection: "row",
@@ -269,7 +239,7 @@ const NoteItem = ({
color={colors.secondary.paragraph} color={colors.secondary.paragraph}
size={SIZE.xs} size={SIZE.xs}
> >
{attachmentCount} {attachmentsCount}
</Paragraph> </Paragraph>
</View> </View>
) : null} ) : null}
@@ -282,10 +252,7 @@ const NoteItem = ({
style={{ style={{
marginRight: 6 marginRight: 6
}} }}
color={ color={color?.colorCode || primaryColors.accent}
ColorValues[item.color?.toLowerCase()] ||
primaryColors.accent
}
/> />
) : null} ) : null}
@@ -314,7 +281,7 @@ const NoteItem = ({
) : null} ) : null}
{!isTrash && !compactMode && tags {!isTrash && !compactMode && tags
? tags.map((item) => ? tags.items?.map((item) =>
item.id ? ( item.id ? (
<Button <Button
title={"#" + item.title} title={"#" + item.title}
@@ -331,9 +298,9 @@ const NoteItem = ({
paddingHorizontal: 6, paddingHorizontal: 6,
marginRight: 4, marginRight: 4,
zIndex: 10, zIndex: 10,
maxWidth: tags.length > 1 ? 130 : null maxWidth: tags.items?.length > 1 ? 130 : null
}} }}
onPress={() => navigateToTag(item)} onPress={() => TaggedNotes.navigate(item, true)}
/> />
) : null ) : null
) )
@@ -361,7 +328,8 @@ const NoteItem = ({
marginRight: 6 marginRight: 6
}} }}
> >
{item.itemType[0].toUpperCase() + item.itemType.slice(1)} {(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
</Paragraph> </Paragraph>
</> </>
)} )}
@@ -417,8 +385,8 @@ const NoteItem = ({
color: colors.secondary.paragraph, color: colors.secondary.paragraph,
marginRight: 6 marginRight: 6
}} }}
time={item[dateBy]} time={date}
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000} updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/> />
</> </>
) : null} ) : null}
@@ -428,7 +396,7 @@ const NoteItem = ({
color={primaryColors.paragraph} color={primaryColors.paragraph}
name="dots-horizontal" name="dots-horizontal"
size={SIZE.xl} size={SIZE.xl}
onPress={() => !noOpen && showActionSheet(item, isTrash)} onPress={() => !noOpen && Properties.present(item)}
customStyle={{ customStyle={{
justifyContent: "center", justifyContent: "center",
height: 35, height: 35,

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 React from "react";
import NoteItem from "."; import NoteItem from ".";
import { notesnook } from "../../../../e2e/test.ids"; 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 { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events"; import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs"; 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 NotePreview from "../../note-history/preview";
import SelectionWrapper from "../selection-wrapper"; import SelectionWrapper from "../selection-wrapper";
const present = () => export const openNote = async (
presentDialog({ item: Note,
title: "Note not synced", isTrash?: boolean,
negativeText: "Ok", isSheet?: boolean
paragraph: "Please sync again to open this note for editing" ) => {
}); let note: Note = item;
export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
let _note = item;
if (isSheet) hideSheet(); if (isSheet) hideSheet();
if (!isTrash) { if (!isTrash) {
_note = db.notes.note(item.id).data; note = (await db.notes.note(item.id)) as Note;
if (!db.notes.note(item.id)?.synced()) {
present();
return;
}
} else {
if (!db.trash.synced(item.id)) {
present();
return;
}
} }
const { selectedItemsList, selectionMode, clearSelection } =
const { selectedItemsList, selectionMode, clearSelection, setSelectedItem } =
useSelectionStore.getState(); useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode) { if (selectedItemsList.length > 0 && selectionMode) {
setSelectedItem && setSelectedItem(_note); setSelectedItem(note);
return; return;
} else { } else {
clearSelection(); clearSelection();
} }
if (_note.conflicted) { if (note.conflicted) {
eSendEvent(eShowMergeDialog, _note); eSendEvent(eShowMergeDialog, note);
return; return;
} }
if (_note.locked) { if (note.locked) {
openVault({ openVault({
item: _note, item: note,
novault: true, novault: true,
locked: true, locked: true,
goToEditor: true, goToEditor: true,
@@ -85,16 +79,18 @@ export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
return; return;
} }
if (isTrash) { 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({ presentSheet({
component: ( component: (
<NotePreview note={item} content={{ type: "tiptap", data: content }} /> <NotePreview note={item} content={{ type: "tiptap", data: content }} />
) )
}); });
} else { } else {
useEditorStore.getState().setReadonly(_note?.readonly); useEditorStore.getState().setReadonly(note?.readonly);
eSendEvent(eOnLoadNote, { eSendEvent(eOnLoadNote, {
item: _note item: note
}); });
if (!DDS.isTab) { if (!DDS.isTab) {
tabBarRef.current?.goToPage(1); tabBarRef.current?.goToPage(1);
@@ -102,33 +98,62 @@ export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
} }
}; };
export const NoteWrapper = React.memo( type NoteWrapperProps = {
function NoteWrapper({ item, index, dateBy, isSheet }) { 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 isTrash = item.type === "trash";
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
return ( return (
<SelectionWrapper <SelectionWrapper
index={index}
height={100}
testID={notesnook.ids.note.get(index)} testID={notesnook.ids.note.get(index)}
onPress={() => openNote(item, isTrash, setSelectedItem, isSheet)} onPress={() => openNote(item as Note, isTrash, isRenderedInActionSheet)}
isSheet={isSheet} isSheet={isRenderedInActionSheet}
item={item} item={item}
color={restProps.color?.colorCode}
> >
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} /> <NoteItem {...restProps} item={item} index={index} isTrash={isTrash} />
</SelectionWrapper> </SelectionWrapper>
); );
}, },
(prev, next) => { (prev, next) => {
if (prev.dateBy !== next.dateBy) { if (prev.date !== next.date) {
return false;
}
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false; 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; return false;
} }

View File

@@ -17,41 +17,38 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 React from "react";
import { View } from "react-native"; import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids"; import { notesnook } from "../../../../e2e/test.ids";
import { TopicNotes } from "../../../screens/notes/topic-notes"; import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size"; import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties"; import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button"; import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading"; import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
const showActionSheet = (item) => { type NotebookItemProps = {
Properties.present(item); item: Notebook | BaseTrashItem<Notebook>;
}; totalNotes: number;
date: number;
const navigateToTopic = (topic) => { index: number;
if (useSelectionStore.getState().selectedItemsList.length > 0) return; isTrash: boolean;
TopicNotes.navigate(topic, true);
}; };
export const NotebookItem = ({ export const NotebookItem = ({
item, item,
isTopic = false,
isTrash, isTrash,
dateBy, date,
totalNotes totalNotes
}) => { }: NotebookItemProps) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(item); const compactMode = useIsCompactModeEnabled(
const topics = item.topics?.slice(0, 3) || []; (item as TrashItem).itemType || item.type
);
return ( return (
<> <>
@@ -83,7 +80,7 @@ export const NotebookItem = ({
</Heading> </Heading>
)} )}
{isTopic || !item.description || compactMode ? null : ( {!item.description || compactMode ? null : (
<Paragraph <Paragraph
size={SIZE.sm} size={SIZE.sm}
numberOfLines={2} numberOfLines={2}
@@ -95,42 +92,6 @@ export const NotebookItem = ({
</Paragraph> </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 ? ( {!compactMode ? (
<View <View
style={{ style={{
@@ -152,7 +113,9 @@ export const NotebookItem = ({
}} }}
> >
{"Deleted on " + {"Deleted on " +
new Date(item.dateDeleted).toISOString().slice(0, 10)} new Date((item as TrashItem).dateDeleted)
.toISOString()
.slice(0, 10)}
</Paragraph> </Paragraph>
<Paragraph <Paragraph
color={colors.primary.accent} color={colors.primary.accent}
@@ -162,7 +125,8 @@ export const NotebookItem = ({
marginRight: 6 marginRight: 6
}} }}
> >
{item.itemType[0].toUpperCase() + item.itemType.slice(1)} {(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
</Paragraph> </Paragraph>
</> </>
) : ( ) : (
@@ -173,7 +137,7 @@ export const NotebookItem = ({
marginRight: 6 marginRight: 6
}} }}
> >
{getFormattedDate(item[dateBy], "date")} {getFormattedDate(date, "date")}
</Paragraph> </Paragraph>
)} )}
<Paragraph <Paragraph
@@ -233,7 +197,7 @@ export const NotebookItem = ({
name="dots-horizontal" name="dots-horizontal"
testID={notesnook.ids.notebook.menu} testID={notesnook.ids.notebook.menu}
size={SIZE.xl} size={SIZE.xl}
onPress={() => showActionSheet(item)} onPress={() => Properties.present(item)}
customStyle={{ customStyle={{
justifyContent: "center", justifyContent: "center",
height: 35, height: 35,

View File

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

View File

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

View File

@@ -22,8 +22,9 @@ import React from "react";
import { View } from "react-native"; import { View } from "react-native";
import useIsSelected from "../../../hooks/use-selected"; import useIsSelected from "../../../hooks/use-selected";
import { useEditorStore } from "../../../stores/use-editor-store"; 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 { colors } = useThemeColors();
const isEditingNote = useEditorStore( const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id (state) => state.currentEditingNote === item.id

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,177 +17,169 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 { useThemeColors } from "@notesnook/theme";
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import React, { useEffect, useRef } from "react"; 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 Animated, { FadeInDown } from "react-native-reanimated";
import { notesnook } from "../../../e2e/test.ids"; import { notesnook } from "../../../e2e/test.ids";
import { useGroupOptions } from "../../hooks/use-group-options"; import { useGroupOptions } from "../../hooks/use-group-options";
import { eSendEvent } from "../../services/event-manager"; import { eSendEvent } from "../../services/event-manager";
import Sync from "../../services/sync"; import Sync from "../../services/sync";
import { RouteName } from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-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 { tabBarRef } from "../../utils/global-refs";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { Footer } from "../list-items/footer"; import { Footer } from "../list-items/footer";
import { Header } from "../list-items/headers/header"; import { Header } from "../list-items/headers/header";
import { SectionHeader } from "../list-items/headers/section-header"; import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper"; import { Empty, PlaceholderData } from "./empty";
import { NotebookWrapper } from "../list-items/notebook/wrapper"; import { ListItemWrapper } from "./list-item.wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
import { Empty } from "./empty";
const renderItems = { type ListProps = {
note: NoteWrapper, data: VirtualizedGrouping<Item> | undefined;
notebook: NotebookWrapper, dataType: Item["type"];
topic: NotebookWrapper, onRefresh?: () => void;
tag: TagItem, loading?: boolean;
section: SectionHeader, headerTitle?: string;
header: SectionHeader, customAccentColor?: string;
reminder: ReminderItem renderedInRoute?: RouteName;
CustomLisHeader?: React.JSX.Element;
isRenderedInActionSheet?: boolean;
CustomListComponent?: React.JSX.ElementType;
placeholder?: PlaceholderData;
}; };
const RenderItem = ({ item, index, type, ...restArgs }) => { export default function List(props: ListProps) {
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
}) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const scrollRef = useRef(); const scrollRef = useRef();
const [notesListMode, notebooksListMode] = useSettingStore((state) => [ const [notesListMode, notebooksListMode] = useSettingStore((state) => [
state.settings.notesListMode, state.settings.notesListMode,
state.settings.notebooksListMode state.settings.notebooksListMode
]); ]);
const isCompactModeEnabled = const isCompactModeEnabled =
(type === "notes" && notesListMode === "compact") || (props.dataType === "note" && notesListMode === "compact") ||
type === "notebooks" || props.dataType === "notebook" ||
notebooksListMode === "compact"; notebooksListMode === "compact";
const groupType = 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 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 () => { const _onRefresh = async () => {
Sync.run("global", false, true, () => { Sync.run("global", false, true, () => {
if (refreshCallback) { props.onRefresh?.();
refreshCallback();
}
}); });
}; };
const _onScroll = React.useCallback( const renderItem = React.useCallback(
(event) => { ({ 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; if (!event) return;
let y = event.nativeEvent.contentOffset.y;
eSendEvent(eScrollEvent, { eSendEvent(eScrollEvent, {
y, y: event.nativeEvent.contentOffset.y,
screen screen: props.renderedInRoute
}); });
}, },
[screen] [props.renderedInRoute]
); );
useEffect(() => { useEffect(() => {
eSendEvent(eScrollEvent, { eSendEvent(eScrollEvent, {
y: 0, y: 0,
screen screen: props.renderedInRoute
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
let styles = { const styles = {
width: "100%", width: "100%",
minHeight: 1, minHeight: 1,
minWidth: 1 minWidth: 1
}; };
const ListView = ScrollComponent ? ScrollComponent : FlashList; const ListView = props.CustomListComponent
? props.CustomListComponent
: FlashList;
return ( return (
<> <>
<Animated.View <Animated.View
style={{ style={{
flex: 1 flex: 1
}} }}
entering={type === "search" ? undefined : FadeInDown} entering={props.renderedInRoute === "Search" ? undefined : FadeInDown}
> >
<ListView <ListView
{...handlers}
style={styles} style={styles}
ref={scrollRef} ref={scrollRef}
testID={notesnook.list.id} testID={notesnook.list.id}
data={listData} data={props.data?.ids || []}
renderItem={renderItem} renderItem={renderItem}
onScroll={_onScroll} onScroll={onListScroll}
nestedScrollEnabled={true} nestedScrollEnabled={true}
onMomentumScrollEnd={() => { onMomentumScrollEnd={() => {
tabBarRef.current?.unlock(); tabBarRef.current?.unlock();
onMomentumScrollEnd?.();
}} }}
getItemType={(item) => item.itemType || item.type} getItemType={(item: any) => (isGroupHeader(item) ? "header" : "item")}
estimatedItemSize={isCompactModeEnabled ? 60 : 100} estimatedItemSize={isCompactModeEnabled ? 60 : 100}
directionalLockEnabled={true} directionalLockEnabled={true}
keyboardShouldPersistTaps="always" keyboardShouldPersistTaps="always"
@@ -202,44 +194,31 @@ const List = ({
/> />
} }
ListEmptyComponent={ ListEmptyComponent={
placeholderData ? ( props.placeholder ? (
<Empty <Empty
loading={loading} loading={props.loading}
placeholderData={placeholderData} title={props.headerTitle}
headerProps={headerProps} dataType={props.dataType}
type={type} color={props.customAccentColor}
screen={screen} placeholder={props.placeholder}
/> />
) : null ) : null
} }
ListFooterComponent={<Footer />} ListFooterComponent={<Footer />}
ListHeaderComponent={ ListHeaderComponent={
<> <>
{ListHeader ? ( {props.CustomLisHeader ? (
ListHeader props.CustomLisHeader
) : !headerProps ? null : ( ) : !props.headerTitle ? null : (
<Header <Header
title={headerProps.heading} color={props.customAccentColor}
color={headerProps.color} screen={props.renderedInRoute}
type={type}
screen={screen}
warning={warning}
/> />
)} )}
</> </>
} }
/> />
</Animated.View> </Animated.View>
{listData ? (
<JumpToSectionDialog
screen={screen}
data={listData}
type={screen === "Home" ? "home" : type}
scrollRef={scrollRef}
/>
) : null}
</> </>
); );
}; }
export default List;

View File

@@ -0,0 +1,269 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
import {
GroupingKey,
Item,
VirtualizedGrouping,
Color,
Reminder,
Notebook,
Tag,
TrashItem,
ItemType,
Note
} from "@notesnook/core";
import { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { NoteWrapper } from "../list-items/note/wrapper";
import { db } from "../../common/database";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
export type WithDateEdited<T> = { items: T[]; dateEdited: number };
export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
export type TagsWithDateEdited = WithDateEdited<Tag>;
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
id: string;
isSheet: boolean;
index: number;
};
export function ListItemWrapper(props: ListItemWrapperProps) {
const { id, items, group, isSheet, index } = props;
const [item, setItem] = useState<Item>();
const tags = useRef<TagsWithDateEdited>();
const notebooks = useRef<NotebooksWithDateEdited>();
const reminder = useRef<Reminder>();
const color = useRef<Color>();
const totalNotes = useRef<number>(0);
const attachmentsCount = useRef(0);
useEffect(() => {
(async function () {
const { item, data } = (await items?.item(id, resolveItems)) || {};
if (!item) return;
if (item.type === "note" && isNoteResolvedData(data)) {
tags.current = data.tags;
notebooks.current = data.notebooks;
reminder.current = data.reminder;
color.current = data.color;
attachmentsCount.current = data.attachmentsCount;
} else if (item.type === "notebook" && typeof data === "number") {
totalNotes.current = data;
} else if (item.type === "tag" && typeof data === "number") {
totalNotes.current = data;
}
setItem(item);
})();
}, [id, items]);
if (!item) return <View style={{ height: 100, width: "100%" }} />;
const type = ((item as TrashItem).itemType || item.type) as ItemType;
switch (type) {
case "note": {
return (
<NoteWrapper
item={item as Note}
tags={tags.current}
color={color.current}
notebooks={notebooks.current}
reminder={reminder.current}
attachmentsCount={attachmentsCount.current}
date={getDate(item, group)}
isRenderedInActionSheet={isSheet}
index={index}
/>
);
}
case "notebook":
return (
<NotebookWrapper
item={item as Notebook}
totalNotes={totalNotes.current}
date={getDate(item, group)}
index={index}
/>
);
case "reminder":
return (
<ReminderItem item={item as Reminder} index={index} isSheet={isSheet} />
);
case "tag":
return (
<TagItem
item={item as Tag}
index={index}
totalNotes={totalNotes.current}
/>
);
default:
return null;
}
}
function withDateEdited<
T extends { dateEdited: number } | { dateModified: number }
>(items: T[]): WithDateEdited<T> {
let latestDateEdited = 0;
items.forEach((item) => {
const date = "dateEdited" in item ? item.dateEdited : item.dateModified;
if (latestDateEdited < date) latestDateEdited = date;
});
return { dateEdited: latestDateEdited, items };
}
function getDate(item: Item, groupType?: GroupingKey): number {
return (
getSortValue(
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},
item
) || 0
);
}
async function resolveItems(ids: string[], items: Record<string, Item>) {
const { type } = items[ids[0]];
if (type === "note") return resolveNotes(ids);
else if (type === "notebook") {
const data: Record<string, number> = {};
for (const id of ids) data[id] = await db.notebooks.totalNotes(id);
return data;
} else if (type === "tag") {
const data: Record<string, number> = {};
for (const id of ids)
data[id] = await db.relations.from({ id, type: "tag" }, "note").count();
return data;
}
return {};
}
type NoteResolvedData = {
notebooks?: NotebooksWithDateEdited;
reminder?: Reminder;
color?: Color;
tags?: TagsWithDateEdited;
attachmentsCount: number;
};
async function resolveNotes(ids: string[]) {
console.time("relations");
const relations = [
...(await db.relations
.to({ type: "note", ids }, ["notebook", "tag", "color"])
.get()),
...(await db.relations.from({ type: "note", ids }, "reminder").get())
];
console.timeEnd("relations");
const relationIds: {
notebooks: Set<string>;
colors: Set<string>;
tags: Set<string>;
reminders: Set<string>;
} = {
colors: new Set(),
notebooks: new Set(),
tags: new Set(),
reminders: new Set()
};
const grouped: Record<
string,
{
notebooks: string[];
color?: string;
tags: string[];
reminder?: string;
}
> = {};
for (const relation of relations) {
const noteId =
relation.toType === "relation" ? relation.fromId : relation.toId;
const data = grouped[noteId] || {
notebooks: [],
tags: []
};
if (relation.toType === "relation" && !data.reminder) {
data.reminder = relation.fromId;
relationIds.reminders.add(relation.fromId);
} else if (relation.fromType === "notebook" && data.notebooks.length < 2) {
data.notebooks.push(relation.fromId);
relationIds.notebooks.add(relation.fromId);
} else if (relation.fromType === "tag" && data.tags.length < 3) {
data.tags.push(relation.fromId);
relationIds.tags.add(relation.fromId);
} else if (relation.fromType === "color" && !data.color) {
data.color = relation.fromId;
relationIds.colors.add(relation.fromId);
}
grouped[relation.toId] = data;
}
console.time("resolve");
const resolved = {
notebooks: await db.notebooks.all.records(
Array.from(relationIds.notebooks)
),
tags: await db.tags.all.records(Array.from(relationIds.tags)),
colors: await db.colors.all.records(Array.from(relationIds.colors)),
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
};
console.timeEnd("resolve");
const data: Record<string, NoteResolvedData> = {};
for (const noteId in grouped) {
const group = grouped[noteId];
data[noteId] = {
color: group.color ? resolved.colors[group.color] : undefined,
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
tags: withDateEdited(group.tags.map((id) => resolved.tags[id])),
notebooks: withDateEdited(
group.notebooks.map((id) => resolved.notebooks[id])
),
attachmentsCount:
(await db.attachments?.ofNote(noteId, "all"))?.length || 0
};
}
return data;
}
function isNoteResolvedData(data: unknown): data is NoteResolvedData {
return (
typeof data === "object" &&
!!data &&
"notebooks" in data &&
"reminder" in data &&
"color" in data &&
"tags" in data
);
}

View File

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

View File

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

View File

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

View File

@@ -53,23 +53,24 @@ export const DateMeta = ({ item }) => {
return keys.filter((key) => key.startsWith("date") && key !== "date"); return keys.filter((key) => key.startsWith("date") && key !== "date");
} }
const renderItem = (key) => ( const renderItem = (key) =>
<View !item[key] ? null : (
key={key} <View
style={{ key={key}
flexDirection: "row", style={{
justifyContent: "space-between", flexDirection: "row",
paddingVertical: 3 justifyContent: "space-between",
}} paddingVertical: 3
> }}
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}> >
{getNameFromKey(key)} <Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
</Paragraph> {getNameFromKey(key)}
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}> </Paragraph>
{getFormattedDate(item[key], "date-time")} <Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
</Paragraph> {getFormattedDate(item[key], "date-time")}
</View> </Paragraph>
); </View>
);
return ( return (
<View <View

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,70 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { View } from "react-native";
import { TopicNotes } from "../../screens/notes/topic-notes";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
export const Topics = ({ item, close }) => {
const open = (topic) => {
close();
TopicNotes.navigate(topic, true);
};
const renderItem = (topic) => (
<Button
key={topic.id}
title={topic.title}
type="grayBg"
// buttonType={{
// text: colors.primary.accent
// }}
height={30}
onPress={() => open(topic)}
icon="bookmark-outline"
fontSize={SIZE.xs}
style={{
marginRight: 5,
paddingHorizontal: 8,
borderRadius: 100,
marginVertical: 5
}}
/>
);
return item &&
item.type === "notebook" &&
item.topics &&
item.topics.length > 0 ? (
<View
style={{
flexDirection: "row",
marginTop: 5,
width: "100%",
flexWrap: "wrap"
}}
>
{item.topics
.sort((a, b) => a.dateEdited - b.dateEdited)
.slice(0, 6)
.map(renderItem)}
</View>
) : null;
};

View File

@@ -1,560 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { createRef } from "react";
import {
Keyboard,
StyleSheet,
TextInput,
TouchableOpacity,
View
} from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import { ToastManager, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE, ph, pv } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
let refs = [];
export class AddNotebookSheet extends React.Component {
constructor(props) {
super(props);
refs = [];
this.state = {
notebook: props.notebook,
topics:
props.notebook?.topics?.map((item) => {
return item.title;
}) || [],
description: null,
titleFocused: false,
descFocused: false,
count: 0,
topicInputFocused: false,
editTopic: false,
loading: false
};
this.title = props.notebook?.title;
this.description = props.notebook?.description;
this.listRef;
this.prevItem = null;
this.prevIndex = null;
this.currentSelectedInput = null;
this.id = props.notebook?.id;
this.backPressCount = 0;
this.currentInputValue = null;
this.titleRef;
this.descriptionRef;
this.topicsToDelete = [];
this.hiddenInput = createRef();
this.topicInputRef = createRef();
this.addingTopic = false;
this.actionSheetRef = props.actionSheetRef;
}
componentWillUnmount() {
refs = [];
}
componentDidMount() {
sleep(300).then(() => {
!this.state.notebook && this.titleRef?.focus();
});
}
close = () => {
refs = [];
this.props.close(true);
};
onDelete = (index) => {
let { topics } = this.state;
let prevTopics = topics;
refs = [];
prevTopics.splice(index, 1);
let edit = this.state.notebook;
if (edit && edit.id) {
let topicToDelete = edit.topics[index];
if (topicToDelete) {
this.topicsToDelete.push(topicToDelete.id);
}
}
let nextTopics = [...prevTopics];
if (this.prevIndex === index) {
this.prevIndex = null;
this.prevItem = null;
this.currentInputValue = null;
this.topicInputRef.current?.setNativeProps({
text: null
});
}
this.setState({
topics: nextTopics
});
};
addNewNotebook = async () => {
this.setState({
loading: true
});
let { topics, notebook } = this.state;
if (!this.title || this.title?.trim().length === 0) {
ToastManager.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
this.setState({
loading: false
});
return;
}
let toEdit = null;
if (notebook) {
toEdit = db.notebooks.notebook(notebook.id).data;
}
let prevTopics = [...topics];
if (this.currentInputValue && this.currentInputValue.trim().length !== 0) {
if (this.prevItem != null) {
prevTopics[this.prevIndex] = this.currentInputValue;
} else {
prevTopics.push(this.currentInputValue);
this.currentInputValue = null;
}
}
let newNotebookId = null;
if (notebook) {
if (this.topicsToDelete?.length > 0) {
await db.notebooks
.notebook(toEdit.id)
.topics.delete(...this.topicsToDelete);
toEdit = db.notebooks.notebook(toEdit.id).data;
}
await db.notebooks.add({
title: this.title,
description: this.description,
id: notebook.id
});
let nextTopics = toEdit.topics.map((topic, index) => {
let copy = { ...topic };
copy.title = prevTopics[index];
return copy;
});
prevTopics.forEach((title, index) => {
if (!nextTopics[index]) {
nextTopics.push(title);
}
});
await db.notebooks.topics(toEdit.id).add(
...nextTopics.map((topic) => ({
title: topic
}))
);
this.close();
} else {
newNotebookId = await db.notebooks.add({
title: this.title,
description: this.description,
topics: prevTopics.map((topic) => ({
title: topic
})),
id: null
});
}
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
};
onSubmit = (forward = true) => {
this.hiddenInput.current?.focus();
let willFocus = true;
let { topics } = this.state;
if (!this.currentInputValue || this.currentInputValue?.trim().length === 0)
return;
let prevTopics = [...topics];
if (this.prevItem === null) {
prevTopics.push(this.currentInputValue);
this.setState({
topics: prevTopics
});
setTimeout(() => {
this.listRef.current?.scrollToEnd?.({ animated: true });
}, 30);
this.currentInputValue = null;
} else {
prevTopics[this.prevIndex] = this.currentInputValue;
this.setState({
topics: prevTopics
});
this.currentInputValue = null;
if (this.state.editTopic) {
this.topicInputRef.current?.blur();
Keyboard.dismiss();
this.setState({
editTopic: false
});
willFocus = false;
}
this.prevItem = null;
this.prevIndex = null;
this.currentInputValue = null;
if (forward) {
setTimeout(() => {
this.listRef.current?.scrollToEnd?.({ animated: true });
}, 30);
}
}
this.topicInputRef.current?.setNativeProps({
text: ""
});
willFocus && this.topicInputRef.current?.focus();
};
renderTopicItem = ({ item, index }) => (
<TopicItem
item={item}
onPress={(item, index) => {
this.prevIndex = index;
this.prevItem = item;
this.topicInputRef.current?.setNativeProps({
text: item
});
this.topicInputRef.current?.focus();
this.currentInputValue = item;
this.setState({
editTopic: true
});
}}
onDelete={this.onDelete}
index={index}
colors={this.props.colors}
/>
);
render() {
const { colors } = this.props;
const { topics, topicInputFocused, notebook } = this.state;
return (
<View
style={{
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
}}
>
<TextInput
ref={this.hiddenInput}
style={{
width: 1,
height: 1,
opacity: 0,
position: "absolute"
}}
blurOnSubmit={false}
/>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>
{notebook && notebook.dateCreated
? "Edit Notebook"
: "New Notebook"}
</Heading>
<Button
title="Save"
type="accent"
height={40}
style={{
borderRadius: 100,
paddingHorizontal: 24
}}
onPress={this.addNewNotebook}
/>
</View>
<Seperator />
<Input
fwdRef={(ref) => (this.titleRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
this.title = value;
}}
placeholder="Enter a title"
onSubmit={() => {
this.descriptionRef.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : null}
/>
<Input
fwdRef={(ref) => (this.descriptionRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
this.description = value;
}}
placeholder="Describe your notebook."
onSubmit={() => {
this.topicInputRef.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : null}
/>
<Input
fwdRef={this.topicInputRef}
testID={notesnook.ids.dialogs.notebook.inputs.topic}
onChangeText={(value) => {
this.currentInputValue = value;
if (this.prevItem !== null) {
refs[this.prevIndex].setNativeProps({
text: value,
style: {
borderBottomColor: colors.primary.accent
}
});
}
}}
returnKeyLabel="Done"
returnKeyType="done"
onSubmit={() => {
this.onSubmit();
}}
blurOnSubmit={false}
button={{
testID: "topic-add-button",
icon: this.state.editTopic ? "check" : "plus",
onPress: this.onSubmit,
color: topicInputFocused
? colors.primary.accent
: colors.secondary.icon
}}
placeholder="Add a topic"
/>
<FlatList
data={topics}
ref={(ref) => (this.listRef = ref)}
nestedScrollEnabled
keyExtractor={(item, index) => item + index.toString()}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
ListFooterComponent={
topics.length === 0 ? null : <View style={{ height: 50 }} />
}
renderItem={this.renderTopicItem}
/>
</View>
);
}
}
AddNotebookSheet.present = (notebook) => {
presentSheet({
component: (ref, close, _update, colors) => (
<AddNotebookSheet
actionSheetRef={ref}
notebook={notebook}
close={close}
colors={colors}
/>
)
});
};
const TopicItem = ({ item, index, colors, onPress, onDelete }) => {
const topicRef = (ref) => (refs[index] = ref);
return (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: colors.secondary.background,
borderRadius: 5,
marginVertical: 5
}}
>
<TouchableOpacity
style={{
width: "80%",
backgroundColor: "transparent",
zIndex: 10,
position: "absolute",
height: 30
}}
onPress={() => {
onPress(item, index);
}}
/>
<TextInput
ref={topicRef}
editable={false}
style={[
styles.topicInput,
{
color: colors.primary.paragraph
}
]}
defaultValue={item}
placeholderTextColor={colors.primary.placeholder}
/>
<View
style={{
width: 80,
position: "absolute",
right: 0,
alignItems: "center",
flexDirection: "row",
justifyContent: "flex-end"
}}
>
<IconButton
onPress={() => {
onPress(item, index);
}}
name="pencil"
size={SIZE.lg - 5}
color={colors.secondary.icon}
/>
<IconButton
onPress={() => {
onDelete(index);
}}
name="minus"
size={SIZE.lg}
color={colors.secondary.icon}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.3)",
justifyContent: "center",
alignItems: "center"
},
overlay: {
width: "100%",
height: "100%",
position: "absolute"
},
headingContainer: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center"
},
headingText: {
marginLeft: 5,
fontSize: SIZE.xl
},
input: {
paddingRight: 12,
paddingHorizontal: 0,
borderRadius: 0,
minHeight: 45,
fontSize: SIZE.md,
padding: pv - 2,
borderBottomWidth: 1,
marginTop: 10,
marginBottom: 5
},
addBtn: {
width: "12%",
minHeight: 45,
justifyContent: "center",
alignItems: "center",
position: "absolute",
right: 0
},
buttonContainer: {
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
width: "100%",
marginTop: 20
},
topicContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: 10
},
topicInput: {
padding: pv - 5,
fontSize: SIZE.sm,
//fontFamily: "sans-serif",
paddingHorizontal: ph,
paddingRight: 40,
paddingVertical: 10,
width: "100%",
maxWidth: "100%"
},
topicBtn: {
borderRadius: 5,
width: 40,
height: 40,
justifyContent: "center",
alignItems: "center",
position: "absolute",
right: 0
}
});

View File

@@ -0,0 +1,173 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Notebook } from "@notesnook/core";
import React, { useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
import { eOnNotebookUpdated } from "../../../utils/events";
export const AddNotebookSheet = ({
notebook,
parentNotebook,
close
}: {
notebook?: Notebook;
parentNotebook?: Notebook;
close: ((context?: string | undefined) => void) | undefined;
}) => {
const title = useRef(notebook?.title);
const description = useRef(notebook?.description);
const titleInput = useRef<TextInput>(null);
const descriptionInput = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const onSaveChanges = async () => {
if (loading) return;
setLoading(true);
if (!title.current || title?.current.trim().length === 0) {
ToastManager.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
setLoading(false);
return;
}
const id = await db.notebooks.add({
title: title.current,
description: description.current,
id: notebook?.id
});
if (parentNotebook) {
await db.relations.add(parentNotebook, {
type: "notebook",
id: id
});
}
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
eSendEvent(eOnNotebookUpdated, parentNotebook?.id);
if (notebook) {
eSendEvent(eOnNotebookUpdated, notebook.id);
}
if (!notebook) {
setTimeout(async () => {
MoveNotes.present(await db.notebooks.notebook(id));
}, 500);
} else {
close?.();
}
};
return (
<View
style={{
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>
{notebook ? "Edit Notebook" : "New Notebook"}
</Heading>
<Button
title={notebook ? "Save" : "Add"}
type="accent"
height={40}
style={{
paddingHorizontal: 24
}}
onPress={onSaveChanges}
/>
</View>
<Seperator />
<Input
fwdRef={titleInput}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
title.current = value;
}}
placeholder="Enter a title"
onSubmit={() => {
descriptionInput.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : ""}
/>
<Input
fwdRef={descriptionInput}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
description.current = value;
}}
placeholder="Describe your notebook."
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : ""}
/>
</View>
);
};
AddNotebookSheet.present = (notebook?: Notebook, parentNotebook?: Notebook) => {
presentSheet({
component: (ref, close) => (
<AddNotebookSheet
notebook={notebook}
parentNotebook={parentNotebook}
close={close}
/>
)
});
};

View File

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

View File

@@ -17,38 +17,47 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 { Keyboard, TouchableOpacity, View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database"; import { db } from "../../../common/database";
import { import { eSendEvent, presentSheet } from "../../../services/event-manager";
eSendEvent,
presentSheet,
ToastManager
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation"; import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search"; import SearchService from "../../../services/search";
import { useNotebookStore } from "../../../stores/use-notebook-store"; import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSelectionStore } from "../../../stores/use-selection-store"; import { useSelectionStore } from "../../../stores/use-selection-store";
import { useSettingStore } from "../../../stores/use-setting-store"; import { useSettingStore } from "../../../stores/use-setting-store";
import { eOnTopicSheetUpdate } from "../../../utils/events"; import { eOnNotebookUpdated } from "../../../utils/events";
import { useThemeColors } from "@notesnook/theme";
import { Dialog } from "../../dialog"; import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header"; import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
import { Button } from "../../ui/button"; import { Button } from "../../ui/button";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { SelectionProvider } from "./context"; import { SelectionProvider } from "./context";
import { FilteredList } from "./filtered-list"; import { FilteredList } from "./filtered-list";
import { ListItem } from "./list-item"; import { ListItem } from "./list-item";
import { useItemSelectionStore } from "./store"; 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 { colors } = useThemeColors();
const notebooks = useNotebookStore((state) => const notebooks = useNotebookStore((state) => state.notebooks);
state.notebooks.filter((n) => n?.type === "notebook")
);
const dimensions = useSettingStore((state) => state.dimensions); const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore( const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList (state) => state.selectedItemsList
@@ -57,90 +66,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
const multiSelect = useItemSelectionStore((state) => state.multiSelect); 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(() => { useEffect(() => {
resetItemState();
return () => { return () => {
useItemSelectionStore.getState().setMultiSelect(false); useItemSelectionStore.getState().setMultiSelect(false);
useItemSelectionStore.getState().setItemState({}); useItemSelectionStore.getState().setItemState({});
@@ -148,68 +74,10 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const resetItemState = useCallback( const updateItemState = useCallback(function (
(state) => { item: Notebook,
const itemState = {}; state: "selected" | "intermediate" | "deselected"
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 itemState = { ...useItemSelectionStore.getState().itemState }; const itemState = { ...useItemSelectionStore.getState().itemState };
const mergeState = { const mergeState = {
[item.id]: state [item.id]: state
@@ -218,11 +86,12 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
...itemState, ...itemState,
...mergeState ...mergeState
}); });
}, []); },
[]);
const contextValue = useMemo( const contextValue = useMemo(
() => ({ () => ({
toggleSelection: (item) => { toggleSelection: (item: Notebook) => {
const itemState = useItemSelectionStore.getState().itemState; const itemState = useItemSelectionStore.getState().itemState;
if (itemState[item.id] === "selected") { if (itemState[item.id] === "selected") {
updateItemState(item, "deselected"); updateItemState(item, "deselected");
@@ -230,68 +99,43 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
updateItemState(item, "selected"); updateItemState(item, "selected");
} }
}, },
deselect: (item) => { deselect: (item: Notebook) => {
updateItemState(item, "deselected"); updateItemState(item, "deselected");
}, },
select: (item) => { select: (item: Notebook) => {
updateItemState(item, "selected"); updateItemState(item, "selected");
}, },
deselectAll: (state) => { deselectAll: () => {
resetItemState(state); 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 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; const itemState = useItemSelectionStore.getState().itemState;
for (const id in 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 (itemState[id] === "selected") {
if (item.type === "notebook") { for (let noteId of noteIds) {
for (let noteId of noteIds) { await db.relations.add(item, { id: noteId, type: "note" });
await db.relations.add(item, { id: noteId, type: "note" });
}
} else {
await db.notes.addToNotebook(
{
topic: item.id,
id: item.notebookId,
rebuildCache: true
},
...noteIds
);
} }
} else if (itemState[id] === "deselected") { } else if (itemState[id] === "deselected") {
if (item.type === "notebook") { for (let noteId of noteIds) {
for (let noteId of noteIds) { await db.relations.unlink(item, { id: noteId, type: "note" });
await db.relations.unlink(item, { id: noteId, type: "note" });
}
} else {
await db.notes.removeFromNotebook(
{
id: item.notebookId,
topic: item.id,
rebuildCache: true
},
...noteIds
);
} }
} }
} }
Navigation.queueRoutesForUpdate(); Navigation.queueRoutesForUpdate();
setNotebooks(); setNotebooks();
eSendEvent(eOnTopicSheetUpdate); eSendEvent(eOnNotebookUpdated);
SearchService.updateAndSearch(); SearchService.updateAndSearch();
useRelationStore.getState().update(); useRelationStore.getState().update();
actionSheetRef.current?.hide(); actionSheetRef.current?.hide();
@@ -360,7 +204,9 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
}} }}
type="grayAccent" type="grayAccent"
onPress={() => { onPress={() => {
resetItemState(); useItemSelectionStore.setState({
itemState: {}
});
}} }}
/> />
</View> </View>
@@ -370,12 +216,12 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
style={{ style={{
paddingHorizontal: 12, paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85, maxHeight: dimensions.height * 0.85,
height: 50 * (notebooks.length + 2) height: 50 * ((notebooks?.ids.length || 0) + 2)
}} }}
> >
<FilteredList <FilteredList
ListEmptyComponent={ ListEmptyComponent={
notebooks.length > 0 ? null : ( notebooks?.ids.length ? null : (
<View <View
style={{ style={{
width: "100%", width: "100%",
@@ -396,7 +242,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
) )
} }
estimatedItemSize={50} estimatedItemSize={50}
data={notebooks} data={notebooks?.ids.length}
hasHeaderSearch={true} hasHeaderSearch={true}
renderItem={({ item, index }) => ( renderItem={({ item, index }) => (
<ListItem <ListItem

View File

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

View File

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

View File

@@ -17,54 +17,52 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useState } from "react"; import React, { RefObject, useEffect, useState } from "react";
import { Platform, useWindowDimensions, View } from "react-native"; import { Platform, View, useWindowDimensions } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet"; import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList"; import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import { db } from "../../../common/database"; import { db } from "../../../common/database";
import { import { presentSheet } from "../../../services/event-manager";
eSendEvent,
presentSheet,
ToastManager
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation"; import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search"; import SearchService from "../../../services/search";
import { eCloseSheet } from "../../../utils/events";
import { SIZE } from "../../../utils/size"; import { SIZE } from "../../../utils/size";
import { Dialog } from "../../dialog"; import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header"; import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
import { Button } from "../../ui/button"; import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button"; import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable"; import { PressableButton } from "../../ui/pressable";
import Seperator from "../../ui/seperator"; import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
export const MoveNotes = ({ export const MoveNotes = ({
notebook, notebook,
selectedTopic,
fwdRef fwdRef
}: { }: {
notebook: Notebook; notebook: Notebook;
selectedTopic?: Topic;
fwdRef: RefObject<ActionSheetRef>; fwdRef: RefObject<ActionSheetRef>;
}) => { }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const [currentNotebook, setCurrentNotebook] = useState(notebook); const [currentNotebook, setCurrentNotebook] = useState(notebook);
const { height } = useWindowDimensions(); const { height } = useWindowDimensions();
let notes = db.notes?.all;
const [selectedNoteIds, setSelectedNoteIds] = useState<string[]>([]); const [selectedNoteIds, setSelectedNoteIds] = useState<string[]>([]);
const [topic, setTopic] = useState(selectedTopic); const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const [existingNoteIds, setExistingNoteIds] = useState<string[]>([]);
notes = notes.filter((note) => { useEffect(() => {
if (!topic) return []; db.notes?.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
const noteIds = db.notes?.topicReferences.get(topic.id); setNotes(notes);
return noteIds.indexOf(note.id) === -1; });
}); db.relations
.from(currentNotebook, "note")
.get()
.then((existingNotes) => {
setExistingNoteIds(
existingNotes.map((existingNote) => existingNote.toId)
);
});
}, [currentNotebook]);
const select = React.useCallback( const select = React.useCallback(
(id: string) => { (id: string) => {
@@ -86,128 +84,20 @@ export const MoveNotes = ({
[selectedNoteIds] [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( const renderItem = React.useCallback(
({ item }: { item: Topic | Note }) => { ({ item }: { item: string }) => {
return ( return (
<PressableButton <SelectableNoteItem
testID="listitem.select" id={item}
onPress={() => { items={notes}
if (item.type == "topic") { select={select}
setTopic(topic || item); selected={selectedNoteIds?.indexOf(item) > -1}
} 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>
); );
}, },
[ [notes, select, selectedNoteIds]
colors.primary.accent,
colors.secondary.paragraph,
colors.primary.paragraph,
colors.selected.icon,
select,
selectedNoteIds,
topic
]
); );
/**
*
*/
return ( return (
<View <View
style={{ style={{
@@ -217,66 +107,12 @@ export const MoveNotes = ({
}} }}
> >
<Dialog context="local" /> <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 <DialogHeader
style={{ title={`Add notes to ${currentNotebook.title}`}
flexDirection: "row", paragraph={"Select the topic in which you would like to move notes."}
justifyContent: "space-between", />
alignItems: "center", <Seperator />
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 />
</>
)}
<FlashList <FlashList
ListEmptyComponent={ ListEmptyComponent={
@@ -288,41 +124,25 @@ export const MoveNotes = ({
}} }}
> >
<Paragraph color={colors.secondary.paragraph}> <Paragraph color={colors.secondary.paragraph}>
{topic ? "No notes to show" : "No topics in this notebook"} No notes to show
</Paragraph> </Paragraph>
{!topic && (
<Button
style={{
marginTop: 10,
height: 40
}}
onPress={() => {
openAddTopicDialog();
}}
title="Add first topic"
type="grayAccent"
/>
)}
</View> </View>
} }
data={topic ? notes : currentNotebook.topics} data={(notes?.ids as string[])?.filter(
(id) => existingNoteIds?.indexOf(id) === -1
)}
renderItem={renderItem} renderItem={renderItem}
/> />
{selectedNoteIds.length > 0 ? ( {selectedNoteIds.length > 0 ? (
<Button <Button
onPress={async () => { onPress={async () => {
if (!topic) return;
await db.notes?.addToNotebook( await db.notes?.addToNotebook(
{ currentNotebook.id,
topic: topic.id,
id: topic.notebookId
},
...selectedNoteIds ...selectedNoteIds
); );
Navigation.queueRoutesForUpdate(); Navigation.queueRoutesForUpdate();
SearchService.updateAndSearch(); SearchService.updateAndSearch();
eSendEvent(eCloseSheet); fwdRef?.current?.hide();
}} }}
title="Move selected notes" title="Move selected notes"
type="accent" 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({ presentSheet({
component: (ref: RefObject<ActionSheetRef>) => ( component: (ref: RefObject<ActionSheetRef>) => (
<MoveNotes fwdRef={ref} notebook={notebook} selectedTopic={topic} /> <MoveNotes fwdRef={ref} notebook={notebook} />
) )
}); });
}; };

View File

@@ -16,174 +16,127 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { GroupHeader, Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import qclone from "qclone";
import React, { import React, {
createContext, createContext,
RefObject,
useCallback,
useContext, useContext,
useEffect, useEffect,
useRef, useRef,
useState useState
} from "react"; } from "react";
import { RefreshControl, useWindowDimensions, View } from "react-native"; import { RefreshControl, View, useWindowDimensions } from "react-native";
import ActionSheet, { import ActionSheet, {
ActionSheetRef, ActionSheetRef,
FlatList FlashList
} from "react-native-actions-sheet"; } 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 Config from "react-native-config";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import create from "zustand";
import { notesnook } from "../../../../e2e/test.ids"; import { notesnook } from "../../../../e2e/test.ids";
import { MMKV } from "../../../common/database/mmkv"; import { MMKV } from "../../../common/database/mmkv";
import { useNotebook } from "../../../hooks/use-notebook";
import NotebookScreen from "../../../screens/notebook";
import { openEditor } from "../../../screens/notes/common"; 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 { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnNotebookUpdated } from "../../../utils/events";
import { deleteItems } from "../../../utils/functions"; import { deleteItems } from "../../../utils/functions";
import { findRootNotebookId } from "../../../utils/notebooks";
import { SIZE, normalize } from "../../../utils/size";
import { Properties } from "../../properties"; 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 Sort from "../sort";
import { GroupedItems, GroupHeader, Topic } from "@notesnook/core/dist/types";
type ConfigItem = { id: string; type: string }; type ConfigItem = { id: string; type: string };
class TopicSheetConfig { class NotebookSheetConfig {
static storageKey: "$$sp"; static storageKey: "$$sp";
static makeId(item: ConfigItem) { static makeId(item: ConfigItem) {
return `${TopicSheetConfig.storageKey}:${item.type}:${item.id}`; return `${NotebookSheetConfig.storageKey}:${item.type}:${item.id}`;
} }
static get(item: ConfigItem) { static get(item: ConfigItem) {
return MMKV.getInt(TopicSheetConfig.makeId(item)) || 0; return MMKV.getInt(NotebookSheetConfig.makeId(item)) || 0;
} }
static set(item: ConfigItem, index = 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 [collapsed, setCollapsed] = useState(false);
const currentScreen = useNavigationStore((state) => state.currentScreen); const currentScreen = useNavigationStore((state) => state.currentScreen);
const canShow = const canShow = currentScreen.name === "Notebook";
currentScreen.name === "Notebook" || currentScreen.name === "TopicNotes"; const [selection, setSelection] = useState<Notebook[]>([]);
const [notebook, setNotebook] = useState(
canShow
? db.notebooks?.notebook(
currentScreen?.notebookId || currentScreen?.id || ""
)?.data
: null
);
const [selection, setSelection] = useState<Topic[]>([]);
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const { colors } = useThemeColors("sheet"); const { colors } = useThemeColors("sheet");
const ref = useRef<ActionSheetRef>(null); 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 currentItem = useRef<string>();
const { fontScale } = useWindowDimensions(); const { fontScale } = useWindowDimensions();
const [groupOptions, setGroupOptions] = useState( const [root, setRoot] = useState<string>();
db.settings.getGroupOptions("topics") const {
); onUpdate: onRequestUpdate,
notebook,
const onRequestUpdate = React.useCallback( nestedNotebooks: notebooks,
(data?: NotebookScreenParams) => { nestedNotebookNotesCount: totalNotes,
if (!canShow) return; groupOptions
if (!data) data = { item: notebook } as NotebookScreenParams; } = useNotebook(currentScreen.name === "Notebook" ? root : undefined);
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 PLACEHOLDER_DATA = { const PLACEHOLDER_DATA = {
heading: "Topics", heading: "Notebooks",
paragraph: "You have not added any topics yet.", paragraph: "You have not added any notebooks yet.",
button: "Add first topic", button: "Add a notebook",
action: () => { action: () => {
if (!notebook) return; if (!notebook) return;
eSendEvent(eOpenAddTopicDialog, { notebookId: notebook.id }); AddNotebookSheet.present(undefined, notebook);
}, },
loading: "Loading notebook topics" loading: "Loading notebook topics"
}; };
const renderTopic = ({ const renderNotebook = ({
item, item,
index index
}: { }: {
item: Topic | GroupHeader; item: string | GroupHeader;
index: number; index: number;
}) => }) =>
(item as GroupHeader).type === "header" ? null : ( (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 = { const selectionContext = {
selection: selection, selection: selection,
enabled, enabled,
setEnabled, setEnabled,
toggleSelection: (item: Topic) => { toggleSelection: (item: Notebook) => {
setSelection((state) => { setSelection((state) => {
const selection = [...state]; const selection = [...state];
const index = selection.findIndex( const index = selection.findIndex(
@@ -204,45 +157,33 @@ export const TopicsSheet = () => {
useEffect(() => { useEffect(() => {
if (canShow) { if (canShow) {
setTimeout(() => { setTimeout(async () => {
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id; const id = currentScreen?.id;
if (currentItem.current !== id) { const nextRoot = await findRootNotebookId(id);
setRoot(nextRoot);
if (nextRoot !== currentItem.current) {
setSelection([]); setSelection([]);
setEnabled(false); setEnabled(false);
} }
currentItem.current = id; currentItem.current = nextRoot;
const notebook = db.notebooks?.notebook(id as string)?.data; const snapPoint = NotebookSheetConfig.get({
const snapPoint = isTopic type: "notebook",
? 0 id: nextRoot as string
: TopicSheetConfig.get({ });
type: isTopic ? "topic" : "notebook",
id: currentScreen.id as string
});
if (ref.current?.isOpen()) { if (ref.current?.isOpen()) {
ref.current?.snapToIndex(snapPoint); ref.current?.snapToIndex(snapPoint);
} else { } else {
ref.current?.show(snapPoint); ref.current?.show(snapPoint);
} }
if (notebook) { onRequestUpdate();
onRequestUpdate({ }, 0);
item: notebook
} as any);
}
}, 300);
} else { } else {
setSelection([]); setSelection([]);
setEnabled(false); setEnabled(false);
ref.current?.hide(); ref.current?.hide();
} }
}, [ }, [canShow, currentScreen?.id, currentScreen.name, onRequestUpdate]);
canShow,
currentScreen?.id,
currentScreen.name,
currentScreen?.notebookId,
onRequestUpdate,
isTopic
]);
return ( return (
<ActionSheet <ActionSheet
@@ -262,9 +203,9 @@ export const TopicsSheet = () => {
}} }}
onSnapIndexChange={(index) => { onSnapIndexChange={(index) => {
setCollapsed(index === 0); setCollapsed(index === 0);
TopicSheetConfig.set( NotebookSheetConfig.set(
{ {
type: isTopic ? "topic" : "notebook", type: "notebook",
id: currentScreen.id as string id: currentScreen.id as string
}, },
index index
@@ -326,7 +267,7 @@ export const TopicsSheet = () => {
}} }}
> >
<Paragraph size={SIZE.xs} color={colors.primary.icon}> <Paragraph size={SIZE.xs} color={colors.primary.icon}>
TOPICS NOTEBOOKS
</Paragraph> </Paragraph>
<View <View
style={{ style={{
@@ -367,7 +308,7 @@ export const TopicsSheet = () => {
} }
onPress={() => { onPress={() => {
presentSheet({ presentSheet({
component: <Sort screen="TopicSheet" type="topics" /> component: <Sort screen="TopicSheet" type="notebook" />
}); });
}} }}
testID="group-topic-button" testID="group-topic-button"
@@ -413,23 +354,24 @@ export const TopicsSheet = () => {
</View> </View>
</View> </View>
<SelectionContext.Provider value={selectionContext}> <SelectionContext.Provider value={selectionContext}>
<FlatList <FlashList
data={topics} data={notebooks?.ids}
style={{ style={{
width: "100%" width: "100%"
}} }}
estimatedItemSize={50}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={false} refreshing={false}
onRefresh={() => { onRefresh={() => {
onRequestUpdate(); eSendEvent(eOnNotebookUpdated);
}} }}
colors={[colors.primary.accent]} colors={[colors.primary.accent]}
progressBackgroundColor={colors.primary.background} progressBackgroundColor={colors.primary.background}
/> />
} }
keyExtractor={(item) => (item as Topic).id} keyExtractor={(item) => item as string}
renderItem={renderTopic} renderItem={renderNotebook}
ListEmptyComponent={ ListEmptyComponent={
<View <View
style={{ style={{
@@ -439,7 +381,7 @@ export const TopicsSheet = () => {
height: 200 height: 200
}} }}
> >
<Paragraph color={colors.primary.icon}>No topics</Paragraph> <Paragraph color={colors.primary.icon}>No notebooks</Paragraph>
</View> </View>
} }
ListFooterComponent={<View style={{ height: 50 }} />} ListFooterComponent={<View style={{ height: 50 }} />}
@@ -451,108 +393,190 @@ export const TopicsSheet = () => {
}; };
const SelectionContext = createContext<{ const SelectionContext = createContext<{
selection: Topic[]; selection: Notebook[];
enabled: boolean; enabled: boolean;
setEnabled: (value: boolean) => void; setEnabled: (value: boolean) => void;
toggleSelection: (item: Topic) => void; toggleSelection: (item: Notebook) => void;
}>({ }>({
selection: [], selection: [],
enabled: false, enabled: false,
setEnabled: (_value: boolean) => {}, setEnabled: (_value: boolean) => {},
toggleSelection: (_item: Topic) => {} toggleSelection: (_item: Notebook) => {}
}); });
const useSelection = () => useContext(SelectionContext); const useSelection = () => useContext(SelectionContext);
const TopicItem = ({ type NotebookParentProp = {
item, parent?: NotebookParentProp;
item?: Notebook;
};
const NotebookItem = ({
id,
totalNotes,
currentLevel = 0,
index, index,
sheetRef parent,
items
}: { }: {
item: Topic; id: string;
totalNotes: (id: string) => number;
currentLevel?: number;
index: 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 screen = useNavigationStore((state) => state.currentScreen);
const { colors } = useThemeColors("sheet"); const { colors } = useThemeColors("sheet");
const selection = useSelection(); const selection = useSelection();
const isSelected = const isSelected =
selection.selection.findIndex((selected) => selected.id === item.id) > -1; selection.selection.findIndex((selected) => selected.id === item?.id) > -1;
const isFocused = screen.id === item.id; const isFocused = screen.id === id;
const notesCount = getTotalNotes(item);
const { fontScale } = useWindowDimensions(); const { fontScale } = useWindowDimensions();
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);
return ( return (
<PressableButton <View
type={isSelected || isFocused ? "selected" : "transparent"} style={{
onLongPress={() => { paddingLeft: currentLevel > 0 && currentLevel < 6 ? 15 : undefined,
if (selection.enabled) return; width: "100%"
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 <PressableButton
style={{ 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", flexDirection: "row",
alignItems: "center" paddingLeft: 0,
paddingRight: 12,
borderRadius: 0
}} }}
> >
{selection.enabled ? ( <View
<IconButton style={{
size={SIZE.lg} flexDirection: "row",
color={isSelected ? colors.selected.icon : colors.primary.icon} alignItems: "center"
top={0} }}
left={0} >
bottom={0} {selection.enabled ? (
right={0} <IconButton
name={ size={SIZE.lg}
isSelected color={isSelected ? colors.selected.icon : colors.primary.icon}
? "check-circle-outline" top={0}
: "checkbox-blank-circle-outline" left={0}
} bottom={0}
/> right={0}
) : null} customStyle={{
<Paragraph size={SIZE.sm}> width: 40,
{item.title}{" "} height: 40
{notesCount ? ( }}
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}> name={
{notesCount} isSelected
</Paragraph> ? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
) : null} ) : null}
</Paragraph>
</View> {nestedNotebooks?.ids.length ? (
<IconButton <IconButton
name="dots-horizontal" size={SIZE.lg}
customStyle={{ color={isSelected ? colors.selected.icon : colors.primary.icon}
width: 40 * fontScale, onPress={() => {
height: 40 * fontScale useNotebookExpandedStore.getState().setExpanded(id);
}} }}
testID={notesnook.ids.notebook.menu} top={0}
onPress={() => { left={0}
Properties.present(item); bottom={0}
}} right={0}
left={0} customStyle={{
right={0} width: 40,
bottom={0} height: 40
top={0} }}
color={colors.primary.icon} name={expanded ? "chevron-down" : "chevron-right"}
size={SIZE.xl} />
/> ) : (
</PressableButton> <>
{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>
); );
}; };

View File

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

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { RefObject } from "react"; import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet"; import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList"; 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 { Button } from "../../ui/button";
import { PressableButtonProps } from "../../ui/pressable"; import { PressableButtonProps } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph"; 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 = { type RelationsListProps = {
actionSheetRef: RefObject<ActionSheetRef>; actionSheetRef: RefObject<ActionSheetRef>;
@@ -73,13 +74,24 @@ export const RelationsList = ({
const updater = useRelationStore((state) => state.updater); const updater = useRelationStore((state) => state.updater);
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const items = const [items, setItems] = useState<VirtualizedGrouping<Item>>();
const hasNoRelations = !items || items?.ids?.length === 0;
useEffect(() => {
db.relations?.[relationType]?.( db.relations?.[relationType]?.(
{ id: item?.id, type: item?.type } as ItemReference, { id: item?.id, type: item?.type } as ItemReference,
referenceType as ItemType referenceType as any
) || []; )
.selector.sorted({
const hasNoRelations = !items || items.length === 0; sortBy: "dateEdited",
sortDirection: "desc",
groupBy: "default"
})
.then((grouped) => {
setItems(grouped);
});
}, [relationType, referenceType]);
return ( return (
<View <View
@@ -119,15 +131,11 @@ export const RelationsList = ({
</View> </View>
) : ( ) : (
<List <List
listData={items} data={items}
ScrollComponent={FlashList} CustomListComponent={FlashList}
loading={false} loading={false}
type={referenceType} dataType={referenceType as any}
headerProps={null} isRenderedInActionSheet={true}
isSheet={true}
onMomentumScrollEnd={() => {
actionSheetRef?.current?.handleChildScrollEnd();
}}
/> />
)} )}
</View> </View>

View File

@@ -17,23 +17,29 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import dayjs from "dayjs"; import dayjs from "dayjs";
import React, { RefObject } from "react"; import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet"; 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 Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database"; import { db } from "../../../common/database";
import { import {
presentSheet, presentSheet,
PresentSheetOptions PresentSheetOptions
} from "../../../services/event-manager"; } from "../../../services/event-manager";
import Notifications, { Reminder } from "../../../services/notifications"; import Notifications from "../../../services/notifications";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size"; import { SIZE } from "../../../utils/size";
import { ItemReference } from "../../../utils/types";
import List from "../../list"; import List from "../../list";
import { Button } from "../../ui/button"; import { Button } from "../../ui/button";
import Heading from "../../ui/typography/heading"; import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import {
Reminder,
ItemReference,
VirtualizedGrouping,
Note
} from "@notesnook/core";
type ReminderSheetProps = { type ReminderSheetProps = {
actionSheetRef: RefObject<ActionSheetRef>; actionSheetRef: RefObject<ActionSheetRef>;
@@ -48,7 +54,16 @@ export default function ReminderNotify({
reminder reminder
}: ReminderSheetProps) { }: ReminderSheetProps) {
const { colors } = useThemeColors(); 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 = [ const QuickActions = [
{ {
@@ -76,7 +91,7 @@ export default function ReminderNotify({
snoozeUntil: snoozeTime snoozeUntil: snoozeTime
}); });
await Notifications.scheduleNotification( await Notifications.scheduleNotification(
db.reminders?.reminder(reminder?.id) await db.reminders?.reminder(reminder?.id as string)
); );
close?.(); close?.();
}; };
@@ -135,12 +150,14 @@ export default function ReminderNotify({
})} })}
</ScrollView> </ScrollView>
{references.length > 0 ? ( {references?.ids && references?.ids?.length > 0 ? (
<View <View
style={{ style={{
width: "100%", width: "100%",
height: height:
160 * references?.length < 500 ? 160 * references?.length : 500, 160 * references?.ids?.length < 500
? 160 * references?.ids?.length
: 500,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: colors.secondary.background, borderTopColor: colors.secondary.background,
marginTop: 5, marginTop: 5,
@@ -157,14 +174,11 @@ export default function ReminderNotify({
REFERENCED IN REFERENCED IN
</Paragraph> </Paragraph>
<List <List
listData={references} data={references}
CustomListComponent={FlashList}
loading={false} loading={false}
type="notes" dataType="note"
headerProps={null} isRenderedInActionSheet={true}
isSheet={true}
onMomentumScrollEnd={() =>
actionSheetRef.current?.handleChildScrollEnd()
}
/> />
</View> </View>
) : null} ) : null}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,131 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
Attachment,
Color,
Note,
Notebook,
Reminder,
Shortcut,
Tag,
VirtualizedGrouping
} from "@notesnook/core";
import React, { useEffect, useState } from "react";
import { db } from "../common/database";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../services/event-manager";
import { eDBItemUpdate } from "../utils/events";
type ItemTypeKey = {
note: Note;
notebook: Notebook;
tag: Tag;
color: Color;
reminder: Reminder;
attachment: Attachment;
shortcut: Shortcut;
};
export const useDBItem = <T extends keyof ItemTypeKey>(
id?: string,
type?: T,
items?: VirtualizedGrouping<ItemTypeKey[T]>
): [ItemTypeKey[T] | undefined, () => void] => {
const [item, setItem] = useState<ItemTypeKey[T]>();
useEffect(() => {
const onUpdateItem = (itemId?: string) => {
if (typeof itemId === "string" && itemId !== id) return;
if (!id) {
setItem(undefined);
return;
}
if (items) {
items.item(id).then((item) => {
setItem(item);
});
} else {
if (!(db as any)[type + "s"][type]) {
console.warn(
"no method found for",
`db.${type}s.${type}(id: string)`
);
} else {
(db as any)[type + "s"]
?.[type]?.(id)
.then((item: ItemTypeKey[T]) => setItem(item));
}
}
};
onUpdateItem();
eSubscribeEvent(eDBItemUpdate, onUpdateItem);
return () => {
eUnSubscribeEvent(eDBItemUpdate, onUpdateItem);
};
}, [id, type]);
return [
item as ItemTypeKey[T],
() => {
if (id) {
eSendEvent(eDBItemUpdate, id);
}
}
];
};
export const useTotalNotes = (
ids: string[],
type: "notebook" | "tag" | "color"
) => {
const [totalNotesById, setTotalNotesById] = useState<{
[id: string]: number;
}>({});
const getTotalNotes = React.useCallback(() => {
if (!ids || !ids.length || !type) return;
db.relations
.from({ type: "notebook", ids: ids as string[] }, ["notebook", "note"])
.get()
.then((relations) => {
const totalNotesById: any = {};
for (const id of ids) {
totalNotesById[id] = relations.filter(
(relation) => relation.fromId === id && relation.toType === "note"
);
}
setTotalNotesById(totalNotesById);
});
}, [ids, type]);
useEffect(() => {
getTotalNotes();
}, [ids, type, getTotalNotes]);
return {
totalNotes: (id: string) => {
return totalNotesById[id] || 0;
},
getTotalNotes: getTotalNotes
};
};

View File

@@ -16,19 +16,18 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ItemType } from "@notesnook/core";
import { useSettingStore } from "../stores/use-setting-store"; import { useSettingStore } from "../stores/use-setting-store";
export function useIsCompactModeEnabled(item: any) { export function useIsCompactModeEnabled(dataType: ItemType) {
const [notebooksListMode, notesListMode] = useSettingStore((state) => [ const [notebooksListMode, notesListMode] = useSettingStore((state) => [
state.settings.notebooksListMode, state.settings.notebooksListMode,
state.settings.notesListMode 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 = dataType === "notebook" ? notebooksListMode : notesListMode;
const listMode = type === "notebook" ? notebooksListMode : notesListMode;
return listMode === "compact"; return listMode === "compact";
} }

View File

@@ -0,0 +1,87 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Notebook, VirtualizedGrouping } from "@notesnook/core";
import React, { useCallback, useEffect, useState } from "react";
import { db } from "../common/database";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { eOnNotebookUpdated } from "../utils/events";
import { useDBItem, useTotalNotes } from "./use-db-item";
export const useNotebook = (
id?: string,
items?: VirtualizedGrouping<Notebook>
) => {
const [item, refresh] = useDBItem(id, "notebook", items);
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions("notebooks")
);
const [notebooks, setNotebooks] = useState<VirtualizedGrouping<Notebook>>();
const { totalNotes: nestedNotebookNotesCount } = useTotalNotes(
notebooks?.ids as string[],
"notebook"
);
const onRequestUpdate = React.useCallback(() => {
if (!item || !id) {
console.log("unset notebook");
setNotebooks(undefined);
return;
}
db.relations
.from(item, "notebook")
.selector.sorted(db.settings.getGroupOptions("notebooks"))
.then((notebooks) => {
setNotebooks(notebooks);
});
}, [item, id]);
useEffect(() => {
onRequestUpdate();
}, [item, onRequestUpdate]);
const onUpdate = useCallback(() => {
setGroupOptions({ ...(db.settings.getGroupOptions("notebooks") as any) });
onRequestUpdate();
}, [onRequestUpdate]);
useEffect(() => {
const onNotebookUpdate = (id?: string) => {
if (typeof id === "string" && id !== id) return;
setImmediate(() => {
onRequestUpdate();
refresh();
});
};
eSubscribeEvent("groupOptionsUpdate", onUpdate);
eSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate);
return () => {
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
eUnSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate);
};
}, [onUpdate, onRequestUpdate, id]);
return {
notebook: item,
nestedNotebookNotesCount,
nestedNotebooks: notebooks,
onUpdate: onRequestUpdate,
groupOptions
};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 { Config } from "react-native-config";
import { db } from "../../common/database"; import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button"; 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 = ({ export const Notebooks = ({
navigation, navigation,
route route
@@ -69,12 +61,6 @@ export const Notebooks = ({
}); });
SearchService.prepareSearch = prepareSearch; SearchService.prepareSearch = prepareSearch;
useNavigationStore.getState().setButtonAction(onPressFloatingButton); 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; return !prev?.current;
}, },
@@ -82,20 +68,34 @@ export const Notebooks = ({
delay: SettingsService.get().homepage === route.name ? 1 : -1 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 ( return (
<DelayLayout delay={1}> <DelayLayout delay={1}>
<List <List
listData={notebooks} data={notebooks}
type="notebooks" dataType="notebook"
screen="Notebooks" renderedInRoute="Notebooks"
loading={!isFocused} loading={!isFocused}
placeholderData={PLACEHOLDER_DATA} placeholder={{
headerProps={{ title: "Your notebooks",
heading: "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 <FloatingButton
title="Create a new notebook" title="Create a new notebook"
onPress={onPressFloatingButton} onPress={onPressFloatingButton}

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 React from "react";
import NotesPage, { PLACEHOLDER_DATA } from "."; import NotesPage, { PLACEHOLDER_DATA } from ".";
import { db } from "../../common/database"; import { db } from "../../common/database";
@@ -34,7 +33,7 @@ export const Monographs = ({
navigation={navigation} navigation={navigation}
route={route} route={route}
get={Monographs.get} get={Monographs.get}
placeholderData={PLACEHOLDER_DATA} placeholder={PLACEHOLDER_DATA}
onPressFloatingButton={openMonographsWebpage} onPressFloatingButton={openMonographsWebpage}
canGoBack={route.params?.canGoBack} canGoBack={route.params?.canGoBack}
focusControl={true} focusControl={true}
@@ -42,11 +41,12 @@ export const Monographs = ({
); );
}; };
Monographs.get = (params?: NotesScreenParams, grouped = true) => { Monographs.get = async (params?: NotesScreenParams, grouped = true) => {
const notes = db.monographs?.all || []; if (!grouped) {
return grouped return await db.monographs.all.items();
? groupArray(notes, db.settings.getGroupOptions("notes")) }
: notes;
return await db.monographs.all.grouped(db.settings.getGroupOptions("notes"));
}; };
Monographs.navigate = (item?: MonographType, canGoBack?: boolean) => { Monographs.navigate = (item?: MonographType, canGoBack?: boolean) => {

View File

@@ -17,14 +17,13 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 React from "react";
import NotesPage, { PLACEHOLDER_DATA } from "."; import NotesPage, { PLACEHOLDER_DATA } from ".";
import { db } from "../../common/database"; import { db } from "../../common/database";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import { NotesScreenParams } from "../../stores/use-navigation-store"; import { NotesScreenParams } from "../../stores/use-navigation-store";
import { openEditor } from "./common"; import { openEditor } from "./common";
import { Tag } from "@notesnook/core/dist/types";
export const TaggedNotes = ({ export const TaggedNotes = ({
navigation, navigation,
route route
@@ -34,7 +33,7 @@ export const TaggedNotes = ({
navigation={navigation} navigation={navigation}
route={route} route={route}
get={TaggedNotes.get} get={TaggedNotes.get}
placeholderData={PLACEHOLDER_DATA} placeholder={PLACEHOLDER_DATA}
onPressFloatingButton={openEditor} onPressFloatingButton={openEditor}
canGoBack={route.params?.canGoBack} canGoBack={route.params?.canGoBack}
focusControl={true} focusControl={true}
@@ -42,11 +41,14 @@ export const TaggedNotes = ({
); );
}; };
TaggedNotes.get = (params: NotesScreenParams, grouped = true) => { TaggedNotes.get = async (params: NotesScreenParams, grouped = true) => {
const notes = db.relations.from(params.item, "note").resolved(); if (!grouped) {
return grouped return await db.relations.from(params.item, "note").resolve();
? groupArray(notes, db.settings.getGroupOptions("notes")) }
: notes;
return await db.relations
.from(params.item, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
}; };
TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => { TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,23 +50,6 @@ import { encodeNonAsciiHTML } from "entities";
import { convertNoteToText } from "../utils/note-to-text"; import { convertNoteToText } from "../utils/note-to-text";
import { Reminder } from "@notesnook/core/dist/types"; 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[] = []; let pinned: DisplayedNotification[] = [];
/** /**
@@ -124,7 +107,9 @@ const onEvent = async ({ type, detail }: Event) => {
const { notification, pressAction, input } = detail; const { notification, pressAction, input } = detail;
if (type === EventType.DELIVERED && Platform.OS === "android") { if (type === EventType.DELIVERED && Platform.OS === "android") {
if (notification?.id) { 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") { if (reminder && reminder.recurringMode === "month") {
await initDatabase(); await initDatabase();
@@ -172,7 +157,7 @@ const onEvent = async ({ type, detail }: Event) => {
case "REMINDER_SNOOZE": { case "REMINDER_SNOOZE": {
await initDatabase(); await initDatabase();
if (!notification?.id) break; if (!notification?.id) break;
const reminder = db.reminders?.reminder( const reminder = await db.reminders?.reminder(
notification?.id?.split("_")[0] notification?.id?.split("_")[0]
); );
if (!reminder) break; if (!reminder) break;
@@ -185,7 +170,7 @@ const onEvent = async ({ type, detail }: Event) => {
snoozeUntil: Date.now() + reminderTime * 60000 snoozeUntil: Date.now() + reminderTime * 60000
}); });
await Notifications.scheduleNotification( await Notifications.scheduleNotification(
db.reminders?.reminder(reminder?.id) await db.reminders?.reminder(reminder?.id)
); );
useRelationStore.getState().update(); useRelationStore.getState().update();
useReminderStore.getState().setReminders(); useReminderStore.getState().setReminders();
@@ -194,7 +179,7 @@ const onEvent = async ({ type, detail }: Event) => {
case "REMINDER_DISABLE": { case "REMINDER_DISABLE": {
await initDatabase(); await initDatabase();
if (!notification?.id) break; if (!notification?.id) break;
const reminder = db.reminders?.reminder( const reminder = await db.reminders?.reminder(
notification?.id?.split("_")[0] notification?.id?.split("_")[0]
); );
await db.reminders?.add({ await db.reminders?.add({
@@ -203,7 +188,7 @@ const onEvent = async ({ type, detail }: Event) => {
}); });
if (!reminder?.id) break; if (!reminder?.id) break;
await Notifications.scheduleNotification( await Notifications.scheduleNotification(
db.reminders?.reminder(reminder?.id) await db.reminders?.reminder(reminder?.id)
); );
useRelationStore.getState().update(); useRelationStore.getState().update();
useReminderStore.getState().setReminders(); useReminderStore.getState().setReminders();
@@ -253,20 +238,7 @@ const onEvent = async ({ type, detail }: Event) => {
const defaultNotebook = db.settings?.getDefaultNotebook(); const defaultNotebook = db.settings?.getDefaultNotebook();
if (defaultNotebook) { if (defaultNotebook) {
if (!defaultNotebook.topic) { await db.notes?.addToNotebook(defaultNotebook, id);
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
);
}
} }
const status = await NetInfo.fetch(); 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; 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 (!note) return;
if (!DDS.isTab && jump) { if (!DDS.isTab && jump) {
tabBarRef.current?.goToPage(1); tabBarRef.current?.goToPage(1);
@@ -871,7 +843,7 @@ async function pinQuickNote(launch: boolean) {
* reschedules them if anything has changed. * reschedules them if anything has changed.
*/ */
async function setupReminders(checkNeedsScheduling = false) { 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(); const triggers = await notifee.getTriggerNotifications();
for (const reminder of reminders) { for (const reminder of reminders) {

View File

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

View File

@@ -17,38 +17,26 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 create, { State } from "zustand";
import { db } from "../common/database"; import { db } from "../common/database";
import { Note, VirtualizedGrouping } from "@notesnook/core";
export interface FavoriteStore extends State { export interface FavoriteStore extends State {
favorites: GroupedItems<Note>; favorites: VirtualizedGrouping<Note> | undefined;
setFavorites: (items?: Note[]) => void; setFavorites: (items?: Note[]) => void;
clearFavorites: () => void; clearFavorites: () => void;
} }
export const useFavoriteStore = create<FavoriteStore>((set, get) => ({ export const useFavoriteStore = create<FavoriteStore>((set) => ({
favorites: [], favorites: undefined,
setFavorites: (items) => { setFavorites: () => {
if (!items) { db.notes.favorites
set({ .grouped(db.settings.getGroupOptions("favorites"))
favorites: groupArray( .then((notes) => {
db.notes.favorites || [], set({
db.settings.getGroupOptions("favorites") favorites: notes
) });
}); });
return;
}
const prev = get().favorites;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const index = prev.findIndex((v) => v.id === item.id);
if (index !== -1) {
prev[index] = item;
}
}
set({ favorites: prev });
}, },
clearFavorites: () => set({ favorites: [] }) clearFavorites: () => set({ favorites: undefined })
})); }));

View File

@@ -17,12 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 create, { State } from "zustand";
import { db } from "../common/database"; import { db } from "../common/database";
export interface MenuStore extends State { export interface MenuStore extends State {
menuPins: []; menuPins: (Notebook | Tag)[];
colorNotes: Color[]; colorNotes: Color[];
setMenuPins: () => void; setMenuPins: () => void;
setColorNotes: () => void; setColorNotes: () => void;
@@ -33,18 +33,16 @@ export const useMenuStore = create<MenuStore>((set) => ({
menuPins: [], menuPins: [],
colorNotes: [], colorNotes: [],
setMenuPins: () => { setMenuPins: () => {
try { db.shortcuts.resolved().then((shortcuts) => {
set({ menuPins: [...(db.shortcuts?.resolved as [])] }); set({ menuPins: [...(shortcuts as [])] });
} catch (e) { });
setTimeout(() => { },
try { setColorNotes: () => {
set({ menuPins: [...(db.shortcuts?.resolved as [])] }); db.colors?.all.items().then((colors) => {
} catch (e) { set({
console.error(e); colorNotes: colors
} });
}, 1000); });
}
}, },
setColorNotes: () => set({ colorNotes: db.colors?.all || [] }),
clearAll: () => set({ menuPins: [], colorNotes: [] }) clearAll: () => set({ menuPins: [], colorNotes: [] })
})); }));

View File

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

View File

@@ -17,38 +17,26 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 create, { State } from "zustand";
import { db } from "../common/database"; import { db } from "../common/database";
import { GroupedItems, Notebook } from "@notesnook/core/dist/types"; import { VirtualizedGrouping, Notebook } from "@notesnook/core";
export interface NotebookStore extends State { export interface NotebookStore extends State {
notebooks: GroupedItems<Notebook>; notebooks: VirtualizedGrouping<Notebook> | undefined;
setNotebooks: (items?: Notebook[]) => void; setNotebooks: (items?: Notebook[]) => void;
clearNotebooks: () => void; clearNotebooks: () => void;
} }
export const useNotebookStore = create<NotebookStore>((set, get) => ({ export const useNotebookStore = create<NotebookStore>((set) => ({
notebooks: [], notebooks: undefined,
setNotebooks: (items) => { setNotebooks: () => {
if (!items) { db.notebooks.roots
set({ .grouped(db.settings.getGroupOptions("notebooks"))
notebooks: groupArray( .then((notebooks) => {
db.notebooks.all || [], set({
db.settings.getGroupOptions("notebooks") notebooks: notebooks
) });
}); });
return;
}
const prev = get().notebooks;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const index = prev.findIndex((v) => v.id === item.id);
if (index !== -1) {
prev[index] = item;
}
}
set({ notebooks: prev });
}, },
clearNotebooks: () => set({ notebooks: [] }) clearNotebooks: () => set({ notebooks: undefined })
})); }));

View File

@@ -17,43 +17,28 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 create, { State } from "zustand";
import { db } from "../common/database"; import { db } from "../common/database";
import { GroupedItems, Note } from "@notesnook/core/dist/types";
export interface NoteStore extends State { export interface NoteStore extends State {
notes: GroupedItems<Note>; notes: VirtualizedGrouping<Note> | undefined;
loading: boolean; loading: boolean;
setLoading: (loading: boolean) => void; setLoading: (loading: boolean) => void;
setNotes: (items?: Note[]) => void; setNotes: () => void;
clearNotes: () => void; clearNotes: () => void;
} }
export const useNoteStore = create<NoteStore>((set, get) => ({ export const useNoteStore = create<NoteStore>((set) => ({
notes: [], notes: undefined,
loading: true, loading: true,
setLoading: (loading) => set({ loading: loading }), setLoading: (loading) => set({ loading: loading }),
setNotes: () => {
setNotes: (items) => { db.notes.all.grouped(db.settings.getGroupOptions("home")).then((notes) => {
if (!items) {
set({ set({
notes: groupArray( notes: notes
db.notes.all || [],
db.settings.getGroupOptions("home")
)
}); });
return; });
}
const prev = get().notes;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const index = prev.findIndex((v) => v.id === item.id);
if (index !== -1) {
prev[index] = item;
}
}
set({ notes: prev });
}, },
clearNotes: () => set({ notes: [] }) clearNotes: () => set({ notes: undefined })
})); }));

View File

@@ -17,26 +17,26 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 create, { State } from "zustand";
import { db } from "../common/database"; import { db } from "../common/database";
import { GroupedItems, Reminder } from "@notesnook/core/dist/types"; import { Reminder, VirtualizedGrouping } from "@notesnook/core";
export interface ReminderStore extends State { export interface ReminderStore extends State {
reminders: GroupedItems<Reminder>; reminders: VirtualizedGrouping<Reminder> | undefined;
setReminders: (items?: Reminder[]) => void; setReminders: (items?: Reminder[]) => void;
cleareReminders: () => void; cleareReminders: () => void;
} }
export const useReminderStore = create<ReminderStore>((set) => ({ export const useReminderStore = create<ReminderStore>((set) => ({
reminders: [], reminders: undefined,
setReminders: () => { setReminders: () => {
set({ db.reminders.all
reminders: groupReminders( .grouped(db.settings.getGroupOptions("reminders"))
(db.reminders?.all as Reminder[]) || [], .then((reminders) => {
db.settings?.getGroupOptions("reminders") set({
) reminders: reminders
}); });
});
}, },
cleareReminders: () => set({ reminders: [] }) cleareReminders: () => set({ reminders: undefined })
})); }));

View File

@@ -17,36 +17,24 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 create, { State } from "zustand";
import { db } from "../common/database"; import { db } from "../common/database";
import { GroupedItems, Tag } from "@notesnook/core/dist/types"; import { Tag, VirtualizedGrouping } from "@notesnook/core";
export interface TagStore extends State { export interface TagStore extends State {
tags: GroupedItems<Tag>; tags: VirtualizedGrouping<Tag> | undefined;
setTags: (items?: Tag[]) => void; setTags: (items?: Tag[]) => void;
clearTags: () => void; clearTags: () => void;
} }
export const useTagStore = create<TagStore>((set, get) => ({ export const useTagStore = create<TagStore>((set) => ({
tags: [], tags: undefined,
setTags: (items) => { setTags: () => {
if (!items) { db.tags.all.grouped(db.settings.getGroupOptions("tags")).then((tags) => {
set({ set({
tags: groupArray(db.tags.all || [], db.settings.getGroupOptions("tags")) tags: tags
}); });
return; });
}
const prev = get().tags;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const index = prev.findIndex((v) => v.id === item.id);
if (index !== -1) {
prev[index] = item;
}
}
set({ tags: prev });
}, },
clearTags: () => set({ tags: [] }) clearTags: () => set({ tags: undefined })
})); }));

View File

@@ -17,38 +17,25 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 create, { State } from "zustand";
import { db } from "../common/database"; import { db } from "../common/database";
import { GroupedItems, TrashItem } from "@notesnook/core/dist/types"; import { GroupedItems, TrashItem } from "@notesnook/core/dist/types";
import { VirtualizedGrouping } from "@notesnook/core";
export interface TrashStore extends State { export interface TrashStore extends State {
trash: GroupedItems<TrashItem>; trash: VirtualizedGrouping<TrashItem> | undefined;
setTrash: (items?: GroupedItems<TrashItem>) => void; setTrash: (items?: GroupedItems<TrashItem>) => void;
clearTrash: () => void; clearTrash: () => void;
} }
export const useTrashStore = create<TrashStore>((set, get) => ({ export const useTrashStore = create<TrashStore>((set, get) => ({
trash: [], trash: undefined,
setTrash: (items) => { setTrash: () => {
if (!items) { db.trash.grouped(db.settings.getGroupOptions("trash")).then((trash) => {
set({ set({
trash: groupArray( trash: trash
(db.trash.all as TrashItem[]) || [],
db.settings.getGroupOptions("trash")
)
}); });
return; });
}
const prev = get().trash;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const index = prev.findIndex((v) => v.id === item.id);
if (index !== -1) {
prev[index] = item;
}
}
set({ trash: prev });
}, },
clearTrash: () => set({ trash: [] }) clearTrash: () => set({ trash: undefined })
})); }));

View File

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

View File

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

View File

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

View File

@@ -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 # v8.android.tools.dir=/home/ammarahm-ed/Repos/notesnook-mobile/node_modules/v8-android-jit-nointl/dist/tools/android
# fdroid # fdroid
fdroidBuild=false fdroidBuild=false
quickSqliteFlags=-DSQLITE_ENABLE_FTS5

View File

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

View File

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

View File

@@ -123,6 +123,13 @@ post_install do |installer|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
end end
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| installer.pods_project.targets.each do |target|
if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle" if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
target.build_configurations.each do |config| target.build_configurations.each do |config|

View File

@@ -29,6 +29,7 @@ mergedConfig.resolver = {
"react-dom": path.join(__dirname, "../node_modules/react-dom"), "react-dom": path.join(__dirname, "../node_modules/react-dom"),
"@notesnook": path.join(__dirname, "../../../packages"), "@notesnook": path.join(__dirname, "../../../packages"),
"@notifee/react-native": path.join(__dirname, "../node_modules/@ammarahmed/notifee-react-native"), "@notifee/react-native": path.join(__dirname, "../node_modules/@ammarahmed/notifee-react-native"),
}, },
resolveRequest: (context, moduleName, platform) => { resolveRequest: (context, moduleName, platform) => {
if (moduleName ==='react') { if (moduleName ==='react') {
@@ -38,6 +39,14 @@ mergedConfig.resolver = {
type: 'sourceFile', 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); return context.resolveRequest(context, moduleName, platform);
} }
}; };

View File

@@ -62,7 +62,8 @@
"react-native-tooltips": "^1.0.3", "react-native-tooltips": "^1.0.3",
"react-native-vector-icons": "9.2.0", "react-native-vector-icons": "9.2.0",
"react-native-webview": "^11.14.1", "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": { "devDependencies": {
"@babel/core": "^7.20.0", "@babel/core": "^7.20.0",

View File

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

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